[
  {
    "path": ".claude/settings.local.json",
    "content": "{\n  \"permissions\": {\n    \"allow\": [\n      \"WebFetch(domain:github.com)\",\n      \"WebFetch(domain:docs.microsoft.com)\",\n      \"Bash(bun build:rust:win:*)\",\n      \"Bash(bun runTest:*)\",\n      \"Bash(bun test:*)\",\n      \"Bash(cargo build:*)\",\n      \"Bash(git show:*)\",\n      \"WebFetch(domain:stackoverflow.com)\",\n      \"WebFetch(domain:www.electronjs.org)\",\n      \"WebFetch(domain:www.electron.build)\",\n      \"Bash(where:*)\",\n      \"Bash(cargo test)\",\n      \"Bash(cargo test:*)\",\n      \"Bash(bun:*)\",\n      \"Bash(cargo clippy:*)\",\n      \"Bash(cargo clean:*)\",\n      \"Bash(awk:*)\",\n      \"WebFetch(domain:connectrpc.com)\",\n      \"Bash(git log:*)\"\n    ],\n    \"deny\": [],\n    \"ask\": []\n  }\n}\n"
  },
  {
    "path": ".cursor/rules/always.mdc",
    "content": "---\ndescription: \nglobs: \nalwaysApply: true\n---\n## Development cycle\n\n1. Before writing any code, come up with an extremely good plan, review the plan, and then ask the user for permission to execute the plan.\n2. Always rely on the user for manually testing\n3. Always use bun as your preferred package manager.\n4. Always rely on the user to install new packages / update new packages\n5. Prefer code that is easy to read and self-documenting. Use comments sparingly.\n6. Never use empty catch statements."
  },
  {
    "path": ".cursor/rules/code-conventions.mdc",
    "content": "---\ndescription: \nglobs: **/*.ts,**/*.tsx\nalwaysApply: false\n---\n## Technologies used\n\n- **React** - Build interactive UIs with React components\n- **Tailwind CSS** - Utility-first CSS framework for rapid UI development\n- **TypeScript** - Full type safety across all packages\n- **PNPM Workspaces** - Fast, disk-efficient package management\n- **ESLint & Prettier** - Code quality tools configured and ready to use\n- **Auth** - We use Auth0\n- **DB** - We use postgres in the server and SQLite in the app\n- **Package manager** We use Bun as our preferred package manager. Please use bun whenever applicable.\n\n# Code Conventions\n\nWhen writing or modifying code in this project, please adhere to the following conventions:\n\n1.  **TypeScript Best Practices**: Follow standard, idiomatic TypeScript coding practices for structure, naming, and types, unless otherwise overridden.\n2.  **Minimal Comments**: Avoid adding comments unless they explain complex logic or non-obvious decisions. Well-written, self-explanatory code is preferred. Do not add comments that merely restate what the code does.\n3.  **Tests as Documentation**: Rely on comprehensive tests (which will be added later if not present) to document the behavior and usage of the code, rather than extensive comments within the code itself.\n4. **File naming conventions**: Use kebab-case when naming directories, TypeScript, and other files.\n\nOnly make the exact changes I request—do not modify, remove, or alter any other code, styling, or page elements unless explicitly instructed. If my request conflicts with existing code, styling, or functionality, or if you anticipate any issues, pause execution and notify me for confirmation before proceeding. Always follow this rule for every modification. If in doubt, ask before making any change.\n"
  },
  {
    "path": ".cursor/rules/react.mdc",
    "content": "---\ndescription: \nglobs: **/*.tsx\nalwaysApply: false\n---\nReact Naming Conventions:\n\n- Use kebab-case for files and directories.\n\nComponents:\n- DO not use 'use client' or 'use server' statements\n- Favor named exports for components\n- Ensure components are modular, reusable, and maintain a clear separation of concerns.\n- Always split React components out so there is only ever one per file\n- Keep logic as low as possible.\n\nUI and Styling:\n\n- Implement responsive design with Tailwind CSS; use a mobile-first approach"
  },
  {
    "path": ".cursor/rules/typescript.mdc",
    "content": "---\ndescription: \nglobs: **/*.ts,**/*.tsx\nalwaysApply: false\n---\n# TypeScript Best Practices\n\n## Type System\n- Prefer interfaces over types for object definitions\n- Use type for unions, intersections, and mapped types\n- NEVER use `any` or `as any` types or coercion\n- Use strict TypeScript configuration\n- Leverage TypeScript's built-in utility types\n- Use generics for reusable type patterns\n\n## Naming Conventions\n- Use PascalCase for type names and interfaces\n- Use camelCase for variables and functions\n- Use UPPER_CASE for constants\n- Use descriptive names with auxiliary verbs (e.g., isLoading, hasError)\n- Prefix interfaces for React props with 'Props' (e.g., ButtonProps)\n\n## Code Organization\n- Keep type definitions close to where they're used\n- Export types and interfaces from dedicated type files when shared\n- Use barrel exports (index.ts) for organizing exports\n- Place shared types in a `types.ts` file\n- Co-locate component props with their components\n\n## Functions\n- Use explicit return types for public functions\n- Use arrow functions for callbacks and methods\n- Implement proper error handling with custom error types\n- Use function overloads for complex type scenarios\n- Prefer async/await over Promises\n- Prefer function declarations over function expressions.\n- Prefer functional programming over classes.\n\n## Best Practices\n- Enable strict mode in tsconfig.json\n- Use readonly for immutable properties\n- Leverage discriminated unions for type safety\n- Use type guards for runtime type checking\n- Implement proper null checking\n- Avoid type assertions unless necessary\n\n## Error Handling\n- Do not proactively add error handling\n- Handle Promise rejections properly\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Mark generated protobuf/buf code as generated\n# This makes GitHub collapse these files by default in PRs\napp/generated/** linguist-generated=true\nserver/src/generated/** linguist-generated=true\n"
  },
  {
    "path": ".github/workflows/app-deploy.yml",
    "content": "# .github/workflows/app-deploy.yml\nname: Run Migrations and Deploy App\n\non:\n  workflow_call:\n    inputs:\n      environment:\n        type: string\n        required: true\n        description: 'The environment to build for'\n  workflow_dispatch:\n    inputs:\n      environment:\n        type: string\n        required: true\n        description: 'The environment to build for'\n\npermissions:\n  contents: read\n  id-token: write\n\njobs:\n  migrate-and-deploy:\n    runs-on: ubuntu-latest\n    environment: ${{ inputs.environment }}\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v3\n\n      - name: Install jq\n        run: sudo apt-get update && sudo apt-get install -y jq\n\n      - name: Configure AWS credentials (OIDC)\n        uses: aws-actions/configure-aws-credentials@v2\n        with:\n          role-to-assume: arn:aws:iam::287641434880:role/ItoGitHubCiCdRole\n          aws-region: us-west-2\n\n      - name: Run migrations\n        run: |\n          aws lambda invoke \\\n            --function-name ${{ vars.AWS_STAGE }}-ItoDb-migration \\\n            --payload '{}' \\\n            out.json\n\n          cat out.json\n\n          if jq -e '.FunctionError' out.json > /dev/null; then\n            echo \"❌ Lambda execution failed:\"\n            echo \"Message: $(jq -r '.errorMessage' out.json)\"\n            exit 1\n          fi\n\n          if jq -e '.errorType' out.json > /dev/null; then\n            echo \"❌ Migration failed:\"\n            echo \"Error: $(jq -r '.errorType' out.json)\"\n            echo \"Message: $(jq -r '.errorMessage' out.json)\"\n            exit 1\n          fi\n\n      - name: Force new ECS deployment\n        run: |\n          aws ecs update-service \\\n            --cluster ${{ vars.AWS_STAGE }}-ito-cluster \\\n            --service ${{ vars.AWS_STAGE }}-ito-service \\\n            --force-new-deployment\n"
  },
  {
    "path": ".github/workflows/autolink-pr-to-issue.yml",
    "content": "name: Auto-link PR to Issue\n\non:\n  pull_request:\n    types: [opened]\n\njobs:\n  autolink:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Extract issue number from branch name\n        id: extract_issue\n        run: |\n          BRANCH_NAME=\"${{ github.head_ref }}\"\n          echo \"Branch name: $BRANCH_NAME\"\n\n          # Extract issue number from branch name pattern ito-XXX\n          if [[ $BRANCH_NAME =~ ^ito-([0-9]+) ]]; then\n            ISSUE_NUMBER=\"${BASH_REMATCH[1]}\"\n            echo \"issue_number=$ISSUE_NUMBER\" >> $GITHUB_OUTPUT\n            echo \"Found issue number: $ISSUE_NUMBER\"\n          else\n            echo \"No issue number found in branch name\"\n            echo \"issue_number=\" >> $GITHUB_OUTPUT\n          fi\n\n      - name: Link PR to Issue\n        if: steps.extract_issue.outputs.issue_number != ''\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          ISSUE_NUMBER: ${{ steps.extract_issue.outputs.issue_number }}\n          PR_NUMBER: ${{ github.event.pull_request.number }}\n        run: |\n          # Check if issue exists\n          ISSUE_EXISTS=$(gh api repos/heyito/ito/issues/$ISSUE_NUMBER --jq '.number' 2>/dev/null || echo \"\")\n\n          if [ -n \"$ISSUE_EXISTS\" ]; then\n            echo \"Linking PR #$PR_NUMBER to issue #$ISSUE_NUMBER\"\n            \n            # Add comment to PR mentioning the issue (creates automatic link)\n            gh api repos/heyito/ito/issues/$PR_NUMBER/comments \\\n              --method POST \\\n              --field body=\"Resolves #$ISSUE_NUMBER\"\n            \n            echo \"Successfully linked PR #$PR_NUMBER to issue #$ISSUE_NUMBER\"\n          else\n            echo \"Issue #$ISSUE_NUMBER does not exist, skipping autolink\"\n          fi\n"
  },
  {
    "path": ".github/workflows/build-image.yml",
    "content": "name: Build and Push Docker Image\n\non:\n  workflow_call:\n    inputs:\n      environment:\n        type: string\n        required: true\n        description: 'The environment to build for'\n  workflow_dispatch:\n    inputs:\n      environment:\n        type: string\n        required: true\n        description: 'The environment to build for'\n\npermissions:\n  contents: read\n  id-token: write\n\njobs:\n  build-and-push:\n    runs-on: ubuntu-latest\n    environment: ${{ inputs.environment }}\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v3\n\n      - name: Configure AWS credentials (OIDC)\n        uses: aws-actions/configure-aws-credentials@v2\n        with:\n          role-to-assume: arn:aws:iam::287641434880:role/ItoGitHubCiCdRole\n          aws-region: us-west-2\n\n      - name: Login to ECR\n        run: |\n          aws ecr get-login-password \\\n            | docker login \\\n                --username AWS \\\n                --password-stdin 287641434880.dkr.ecr.us-west-2.amazonaws.com\n\n      - name: Build & push multi-arch image\n        working-directory: server\n        run: |\n          docker buildx create --use\n          docker buildx build \\\n            --platform linux/arm64,linux/amd64 \\\n            --tag 287641434880.dkr.ecr.us-west-2.amazonaws.com/${{ vars.AWS_STAGE }}-ito-server:latest \\\n            --push .\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build and Release\n\non:\n  release:\n    types: [published]\n\npermissions:\n  contents: write\n  id-token: write # required for OIDC role assumption\n\njobs:\n  build-mac:\n    runs-on: macos-latest\n    environment: ${{ github.event.release.target_commitish == 'main' && 'production' || 'develop' }}\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Set up Bun\n        uses: oven-sh/setup-bun@v1\n\n      - name: Install dependencies\n        run: bun install\n\n      - name: Set up Rust\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: stable\n          targets: 'x86_64-apple-darwin,aarch64-apple-darwin'\n\n      - name: Set up environment\n        run: |\n          echo \"VITE_AUTH0_DOMAIN=\\\"${{ secrets.VITE_AUTH0_DOMAIN }}\\\"\" >> .env\n          echo \"VITE_AUTH0_CLIENT_ID=\\\"${{ secrets.VITE_AUTH0_CLIENT_ID }}\\\"\" >> .env\n          echo \"VITE_AUTH0_AUDIENCE=\\\"${{ secrets.VITE_AUTH0_AUDIENCE }}\\\"\" >> .env\n          echo \"VITE_POSTHOG_API_KEY=\\\"${{ secrets.VITE_POSTHOG_API_KEY }}\\\"\" >> .env\n          echo \"VITE_POSTHOG_HOST=\\\"${{ secrets.VITE_POSTHOG_HOST }}\\\"\" >> .env\n          echo \"VITE_GRPC_BASE_URL=\\\"${{ vars.VITE_GRPC_BASE_URL }}\\\"\" >> .env\n          echo \"VITE_UPDATER_BUCKET=\\\"${{ vars.VITE_UPDATER_BUCKET }}\\\"\" >> .env\n          echo \"VITE_SENTRY_DSN=\\\"${{ vars.VITE_SENTRY_DSN }}\\\"\" >> .env\n          echo \"VITE_SENTRY_ENV=\\\"${{ vars.VITE_SENTRY_ENV }}\\\"\" >> .env\n          echo \"VITE_SENTRY_TRACES_SAMPLE_RATE=\\\"${{ vars.VITE_SENTRY_TRACES_SAMPLE_RATE }}\\\"\" >> .env\n          echo \"VITE_SENTRY_PROFILES_SAMPLE_RATE=\\\"${{ vars.VITE_SENTRY_PROFILES_SAMPLE_RATE }}\\\"\" >> .env\n          echo \"VITE_ITO_VERSION=\\\"${GITHUB_REF#refs/tags/v}\\\"\" >> .env\n          echo \"ITO_ENV=\\\"${{ github.event.release.target_commitish == 'main' && 'prod' || 'dev' }}\\\"\" >> .env\n          echo \"VITE_ITO_ENV=\\\"${{ github.event.release.target_commitish == 'main' && 'prod' || 'dev' }}\\\"\" >> .env\n          echo \"APPLE_ID=\\\"${{ secrets.APPLE_ID }}\\\"\" >> .env\n          echo \"APPLE_TEAM_ID=\\\"${{ secrets.APPLE_TEAM_ID }}\\\"\" >> .env\n          echo \"APPLE_APP_SPECIFIC_PASSWORD=\\\"${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}\\\"\" >> .env\n          echo \"CSC_LINK=\\\"release.p12\\\"\" >> .env\n          echo \"CSC_KEY_PASSWORD=\\\"${{ secrets.MACOS_CERT_PASSWORD }}\\\"\" >> .env\n          echo \"GH_TOKEN=\\\"${{ secrets.GITHUB_TOKEN }}\\\"\" >> .env\n          echo \"GRPC_BASE_URL=\\\"${{ vars.GRPC_BASE_URL }}\\\"\" >> .env\n          echo \"Created .env file:\"\n          cat .env\n\n      - name: Decode and install certificate\n        run: |\n          echo \"${{ secrets.MACOS_CERT_BASE64 }}\" | base64 --decode > release.p12\n\n      - name: Build and package macOS application\n        env:\n          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}\n        run: ./build-app.sh mac\n\n      - name: Upload Mac Installer DMG\n        uses: actions/upload-artifact@v4\n        with:\n          name: Ito-Mac-Installer\n          path: dist/Ito-${{ github.event.release.target_commitish == 'main' && 'Installer' || 'dev-Installer' }}.dmg\n\n      - name: Upload Mac Build Artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: Mac-Build-Artifacts\n          path: |\n            dist/*.yml\n            dist/*universal-mac.zip\n            dist/*universal-mac.zip.blockmap\n            dist/*.dmg\n            dist/*.dmg.blockmap\n\n  build-windows-rust:\n    runs-on: windows-latest\n    environment: ${{ github.event.release.target_commitish == 'main' && 'production' || 'develop' }}\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Set up Rust with MSVC toolchain\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: stable-x86_64-pc-windows-msvc\n          targets: 'x86_64-pc-windows-msvc'\n\n      - name: Build Rust native modules with MSVC\n        shell: pwsh\n        run: |\n          # Find Visual Studio installation (GitHub Actions uses Enterprise)\n          $vsPath = if (Test-Path \"C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\") {\n            \"C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\"\n          } elseif (Test-Path \"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\") {\n            \"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\"\n          } else {\n            throw \"Visual Studio 2022 not found\"\n          }\n\n          Write-Host \"Using Visual Studio at: $vsPath\"\n\n          # Launch VS Developer PowerShell and build\n          & \"$vsPath\\Common7\\Tools\\Launch-VsDevShell.ps1\" -Arch amd64\n\n          # Build each native module\n          $modules = @(\"global-key-listener\", \"audio-recorder\", \"text-writer\", \"active-application\", \"selected-text-reader\")\n\n          foreach ($module in $modules) {\n            Write-Host \"Building $module with MSVC...\"\n            Set-Location \"native\\$module\"\n            cargo build --release --target x86_64-pc-windows-msvc\n            Set-Location ..\\..\n          }\n\n      - name: Prepare artifacts directory\n        shell: pwsh\n        run: |\n          New-Item -ItemType Directory -Force -Path rust-binaries\n\n          $modules = @(\"global-key-listener\", \"audio-recorder\", \"text-writer\", \"active-application\", \"selected-text-reader\")\n\n          foreach ($module in $modules) {\n            $sourcePath = \"native\\target\\x86_64-pc-windows-msvc\\release\\$module.exe\"\n            $destPath = \"rust-binaries\\$module.exe\"\n\n            if (Test-Path $sourcePath) {\n              Copy-Item $sourcePath $destPath\n              Write-Host \"Copied $module.exe\"\n            } else {\n              Write-Error \"$sourcePath not found!\"\n              exit 1\n            }\n          }\n\n      - name: Upload Rust binaries\n        uses: actions/upload-artifact@v4\n        with:\n          name: Windows-Rust-Binaries\n          path: rust-binaries/*.exe\n\n  build-windows:\n    runs-on: ubuntu-latest\n    needs: build-windows-rust\n    environment: ${{ github.event.release.target_commitish == 'main' && 'production' || 'develop' }}\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Download Rust binaries built with MSVC\n        uses: actions/download-artifact@v4\n        with:\n          name: Windows-Rust-Binaries\n          path: rust-binaries-msvc\n\n      - name: Place Rust binaries in expected locations\n        run: |\n          # Place binaries where electron-builder expects them\n          for module in global-key-listener audio-recorder text-writer active-application selected-text-reader; do\n            mkdir -p \"native/target/x86_64-pc-windows-msvc/release\"\n            cp \"rust-binaries-msvc/$module.exe\" \"native/target/x86_64-pc-windows-msvc/release/\"\n            echo \"Placed $module.exe\"\n          done\n\n      - name: Set up Bun\n        uses: oven-sh/setup-bun@v1\n\n      - name: Install dependencies\n        run: bun install\n\n      - name: Set up environment\n        run: |\n          echo \"VITE_AUTH0_DOMAIN=\\\"${{ secrets.VITE_AUTH0_DOMAIN }}\\\"\" >> .env\n          echo \"VITE_AUTH0_CLIENT_ID=\\\"${{ secrets.VITE_AUTH0_CLIENT_ID }}\\\"\" >> .env\n          echo \"VITE_AUTH0_AUDIENCE=\\\"${{ secrets.VITE_AUTH0_AUDIENCE }}\\\"\" >> .env\n          echo \"VITE_POSTHOG_API_KEY=\\\"${{ secrets.VITE_POSTHOG_API_KEY }}\\\"\" >> .env\n          echo \"VITE_POSTHOG_HOST=\\\"${{ secrets.VITE_POSTHOG_HOST }}\\\"\" >> .env\n          echo \"VITE_GRPC_BASE_URL=\\\"${{ vars.VITE_GRPC_BASE_URL }}\\\"\" >> .env\n          echo \"VITE_UPDATER_BUCKET=\\\"${{ vars.VITE_UPDATER_BUCKET }}\\\"\" >> .env\n          echo \"VITE_SENTRY_DSN=\\\"${{ vars.VITE_SENTRY_DSN }}\\\"\" >> .env\n          echo \"VITE_SENTRY_ENV=\\\"${{ vars.VITE_SENTRY_ENV }}\\\"\" >> .env\n          echo \"VITE_SENTRY_TRACES_SAMPLE_RATE=\\\"${{ vars.VITE_SENTRY_TRACES_SAMPLE_RATE }}\\\"\" >> .env\n          echo \"VITE_SENTRY_PROFILES_SAMPLE_RATE=\\\"${{ vars.VITE_SENTRY_PROFILES_SAMPLE_RATE }}\\\"\" >> .env\n          echo \"VITE_ITO_VERSION=\\\"${GITHUB_REF#refs/tags/v}\\\"\" >> .env\n          echo \"ITO_ENV=\\\"${{ github.event.release.target_commitish == 'main' && 'prod' || 'dev' }}\\\"\" >> .env\n          echo \"VITE_ITO_ENV=\\\"${{ github.event.release.target_commitish == 'main' && 'prod' || 'dev' }}\\\"\" >> .env\n          echo \"GH_TOKEN=\\\"${{ secrets.GITHUB_TOKEN }}\\\"\" >> .env\n          echo \"GRPC_BASE_URL=\\\"${{ vars.GRPC_BASE_URL }}\\\"\" >> .env\n          echo \"Created .env file:\"\n          cat .env\n\n      - name: Build and package Windows application (unsigned)\n        env:\n          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}\n        run: ./build-app.sh windows --skip-binaries\n\n      - name: Upload unsigned Windows artifacts for signing\n        uses: actions/upload-artifact@v4\n        with:\n          name: Windows-Unsigned-Artifacts\n          path: |\n            dist/*.exe\n            dist/*.yml\n            dist/*.nsis.7z\n            dist/*.zip\n            dist/*.blockmap\n\n  sign-windows:\n    runs-on: windows-latest\n    needs: build-windows\n    environment: ${{ github.event.release.target_commitish == 'main' && 'production' || 'develop' }}\n\n    steps:\n      - name: Download unsigned Windows artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: Windows-Unsigned-Artifacts\n          path: dist\n\n      - name: Sign Windows executable with Azure Trusted Signing\n        uses: Azure/trusted-signing-action@v0\n        with:\n          azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}\n          azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}\n          azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}\n          endpoint: https://eus.codesigning.azure.net/\n          trusted-signing-account-name: ${{ secrets.AZURE_CODE_SIGNING_ACCOUNT_NAME }}\n          certificate-profile-name: ${{ secrets.AZURE_CODE_SIGNING_CERTIFICATE_PROFILE_NAME }}\n          files-folder: dist\n          files-folder-filter: exe\n          file-digest: SHA256\n          timestamp-rfc3161: http://timestamp.acs.microsoft.com\n          timestamp-digest: SHA256\n\n      - name: Regenerate metadata after signing\n        shell: bash\n        run: |\n          # Find the signed executable file in the dist directory\n          # This searches for any file matching the pattern \"Ito-*.exe\"\n          SIGNED_EXE=$(find dist -name \"Ito-*.exe\" | head -1)\n          if [ -z \"$SIGNED_EXE\" ]; then\n            echo \"Error: No signed executable found\"\n            exit 1\n          fi\n\n          echo \"Found signed executable: $SIGNED_EXE\"\n\n          # Calculate the SHA512 checksum of the signed file\n          # cut -d' ' -f1 extracts just the hash part (before the filename)\n          NEW_CHECKSUM=$(sha512sum \"$SIGNED_EXE\" | cut -d' ' -f1)\n\n          # Convert hex checksum to base64 format (required by electron-updater)\n          # xxd -r -p converts hex string to raw bytes\n          # base64 -w 0 encodes to base64 without line wrapping\n          NEW_CHECKSUM_B64=$(echo -n \"$NEW_CHECKSUM\" | xxd -r -p | base64 -w 0)\n\n          # Get the file size in bytes using stat\n          FILE_SIZE=$(stat -c%s \"$SIGNED_EXE\")\n\n          # Extract just the filename from the full path\n          FILENAME=$(basename \"$SIGNED_EXE\")\n\n          echo \"New checksum (hex): $NEW_CHECKSUM\"\n          echo \"New checksum (base64): $NEW_CHECKSUM_B64\"\n          echo \"File size: $FILE_SIZE\"\n          echo \"Filename: $FILENAME\"\n\n          # Update the auto-updater metadata file with the signed file's information\n          # This is critical because signing changes the file's checksum\n          if [ -f \"dist/latest.yml\" ]; then\n            echo \"Updating latest.yml with new checksum after signing\"\n\n            # Update each field in latest.yml using sed (stream editor)\n            # The | delimiter is used instead of / to avoid conflicts with filenames\n            sed -i \"s|url: .*|url: $FILENAME|\" dist/latest.yml          # Update download URL\n            sed -i \"s|sha512: .*|sha512: $NEW_CHECKSUM_B64|\" dist/latest.yml  # Update checksum\n            sed -i \"s|size: .*|size: $FILE_SIZE|\" dist/latest.yml        # Update file size\n            sed -i \"s|path: .*|path: $FILENAME|\" dist/latest.yml         # Update path\n\n            echo \"Updated latest.yml contents:\"\n            cat dist/latest.yml\n          else\n            echo \"Warning: latest.yml not found in dist directory\"\n            ls -la dist/\n          fi\n\n      - name: Upload signed Windows artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: Windows-Build-Artifacts\n          path: |\n            dist/*.yml\n            dist/*.exe\n            dist/*.nsis.7z\n            dist/*.zip\n            dist/*.blockmap\n\n  upload-to-s3:\n    runs-on: ubuntu-latest\n    needs: [build-mac, sign-windows]\n    environment: ${{ github.event.release.target_commitish == 'main' && 'production' || 'develop' }}\n\n    steps:\n      - name: Download Mac Build Artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: Mac-Build-Artifacts\n          path: mac-dist\n\n      - name: Download Windows Build Artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: Windows-Build-Artifacts\n          path: windows-dist\n\n      - name: Configure AWS credentials (OIDC)\n        uses: aws-actions/configure-aws-credentials@v2\n        with:\n          role-to-assume: arn:aws:iam::287641434880:role/ItoGitHubCiCdRole\n          aws-region: us-west-2\n\n      - name: Upload Build Output to S3\n        run: |\n          echo \"Deploying version ${{ github.ref_name }} to S3\"\n\n          BUCKET=s3://${{ vars.AWS_STAGE }}-ito-releases/releases\n\n          echo \"Listing existing files in root of releases/\"\n          aws s3 ls \"$BUCKET/\" | grep -vE '/$' | awk '{print $4}' > existing_files.txt\n\n          echo \"Combining Mac and Windows artifacts\"\n          mkdir -p combined-dist\n\n          # Copy Mac artifacts\n          if [ -d \"mac-dist\" ]; then\n            cp -r mac-dist/* combined-dist/\n          fi\n\n          # Copy Windows artifacts\n          if [ -d \"windows-dist\" ]; then\n            cp -r windows-dist/* combined-dist/\n          fi\n\n          echo \"Files to upload:\"\n          ls -la combined-dist/\n\n          echo \"Identifying which files to delete post-upload\"\n          find combined-dist -maxdepth 1 \\( \\\n            -name '*.yml' -o \\\n            -name '*universal-mac.zip' -o \\\n            -name '*universal-mac.zip.blockmap' -o \\\n            -name '*.dmg' -o \\\n            -name '*.dmg.blockmap' -o \\\n            -name '*.exe' -o \\\n            -name '*.nsis.7z' -o \\\n            -name '*.zip' -o \\\n            -name '*.blockmap' \\\n          \\) | xargs -I{} basename {} > uploaded_root_files.txt\n\n          echo \"Uploading full combined dist to versioned folder: $BUCKET/${{ github.ref_name }}/\"\n          aws s3 cp combined-dist $BUCKET/${{ github.ref_name }}/ --recursive\n\n          echo \"Uploading selected files to root of releases/\"\n          for FILE in \\\n            $(find combined-dist -maxdepth 1 -name '*.yml') \\\n            $(find combined-dist -maxdepth 1 -name '*universal-mac.zip') \\\n            $(find combined-dist -maxdepth 1 -name '*universal-mac.zip.blockmap') \\\n            $(find combined-dist -maxdepth 1 -name '*.dmg') \\\n            $(find combined-dist -maxdepth 1 -name '*.dmg.blockmap') \\\n            $(find combined-dist -maxdepth 1 -name '*.exe') \\\n            $(find combined-dist -maxdepth 1 -name '*.nsis.7z') \\\n            $(find combined-dist -maxdepth 1 -name '*.zip') \\\n            $(find combined-dist -maxdepth 1 -name '*.blockmap')\n          do\n            if [ -f \"$FILE\" ]; then\n              aws s3 cp \"$FILE\" $BUCKET/\n            fi\n          done\n\n          # Compare and find stale files (exist in bucket but not in upload list)\n          # Exclude .blockmap files from deletion to preserve differential update capability\n          echo \"Existing files in bucket root:\"\n          cat existing_files.txt\n          echo \"\"\n          echo \"Files uploaded to bucket root:\"\n          cat uploaded_root_files.txt\n          echo \"\"\n          echo \"Computing stale files (existing - uploaded)...\"\n          comm -23 <(sort existing_files.txt) <(sort uploaded_root_files.txt) > all_stale_files.txt\n          echo \"All stale files before filtering:\"\n          cat all_stale_files.txt || echo \"(none)\"\n          echo \"\"\n          echo \"Filtering out .blockmap files...\"\n          grep -v '\\.blockmap$' all_stale_files.txt > stale_files.txt || echo \"(no non-blockmap stale files found)\"\n\n          echo \"Stale files identified for deletion (excluding .blockmap files for differential updates):\"\n          cat stale_files.txt || echo \"(none)\"\n\n          # Delete each stale file (but preserve blockmaps)\n          while IFS= read -r file; do\n            echo \"Deleting stale file: $file\"\n            aws s3 rm \"$BUCKET/$file\"\n          done < stale_files.txt\n\n      - name: Invalidate CDN Cache for Release Files\n        run: |\n          echo \"Invalidating cache for release files on distribution: ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }}. Updated files should be available within 15 minutes.\"\n\n          aws cloudfront create-invalidation \\\n            --distribution-id \"${{ vars.CLOUDFRONT_DISTRIBUTION_ID }}\" \\\n            --paths \"/*.yml\" \"/*.dmg\" \"/*.exe\" \"/*.zip\" \"/*.blockmap\"\n\n      - name: Upload installers to GitHub Release\n        uses: softprops/action-gh-release@v1\n        with:\n          tag_name: ${{ github.ref_name }}\n          files: |\n            mac-dist/*.dmg\n            windows-dist/*.exe\n"
  },
  {
    "path": ".github/workflows/ci-controller.yml",
    "content": "name: CI Controller\n\non:\n  push:\n    branches:\n      - 'main'\n      - 'dev'\n  pull_request:\n    branches:\n      - '**'\n\njobs:\n  run-tests:\n    uses: ./.github/workflows/test-runner.yml\n\n  native-build-check:\n    uses: ./.github/workflows/native-build-check.yml\n\n  # This job ONLY runs for pushes to 'dev' and 'main'\n  deploy-server:\n    needs: run-tests\n    if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'dev')\n    uses: ./.github/workflows/deploy-server.yml\n    with:\n      # Dynamically set the environment based on the branch\n      environment: ${{ github.ref_name == 'main' && 'production' || 'develop' }}\n    secrets: inherit\n"
  },
  {
    "path": ".github/workflows/deploy-server.yml",
    "content": "name: Deploy Server Worfklow\n\non:\n  workflow_call:\n    inputs:\n      environment:\n        type: string\n        required: true\n\njobs:\n  paths-check:\n    runs-on: ubuntu-latest\n    outputs:\n      server_changed: ${{ steps.filter.outputs.server }}\n      infra_changed: ${{ steps.filter.outputs.infra }}\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 0 # Fetch full history to allow comparing against previous commits\n      - name: Detect changed areas\n        id: filter\n        uses: dorny/paths-filter@v2\n        with:\n          base: ${{ github.event.before }}\n          filters: |\n            server:\n              - server/src/**\n              - server/Dockerfile\n              - server/package.json\n            infra:\n              - server/infra/**\n          list-files: 'none'\n\n  # Step 1: Build and push Docker image first (if server code changed)\n  # This ensures :latest tag is updated before CDK references it\n  build-image:\n    needs: paths-check\n    if: ${{ needs.paths-check.outputs.server_changed == 'true' }}\n    uses: ./.github/workflows/build-image.yml\n    with:\n      environment: ${{ inputs.environment }}\n    secrets: inherit\n\n  # Step 2: Deploy infrastructure (always runs if anything changed)\n  # If task definition changes, ECS will automatically restart with new :latest\n  deploy-infra:\n    needs:\n      - paths-check\n      - build-image\n    if: ${{ always() && (needs.paths-check.outputs.infra_changed == 'true' || needs.paths-check.outputs.server_changed == 'true') && (needs.build-image.result == 'success' || needs.build-image.result == 'skipped') }}\n    uses: ./.github/workflows/infra-deploy.yml\n    with:\n      environment: ${{ inputs.environment }}\n    secrets: inherit\n\n  # Step 3: Run migrations and force ECS deployment (only if server changed)\n  # Migration + force-new-deployment ensures ECS picks up new image even if task def didn't change\n  deploy-app:\n    needs:\n      - paths-check\n      - build-image\n      - deploy-infra\n    if: ${{ needs.paths-check.outputs.server_changed == 'true' }}\n    uses: ./.github/workflows/app-deploy.yml\n    with:\n      environment: ${{ inputs.environment }}\n    secrets: inherit\n"
  },
  {
    "path": ".github/workflows/infra-deploy.yml",
    "content": "name: Infra CDK\n\non:\n  workflow_call:\n    inputs:\n      environment:\n        type: string\n        required: true\n        description: 'The environment to build for'\n  workflow_dispatch:\n    inputs:\n      environment:\n        type: string\n        required: true\n        description: 'The environment to build for'\n\npermissions:\n  contents: read\n  id-token: write # for OIDC\n\njobs:\n  cdk-deploy:\n    runs-on: ubuntu-latest\n    environment: ${{ inputs.environment }}\n\n    steps:\n      - name: Checkout infra\n        uses: actions/checkout@v3\n\n      - name: Setup Node.js & CDK\n        uses: actions/setup-node@v3\n        with:\n          node-version: '20'\n\n      - name: Install infra dependencies\n        working-directory: server/infra\n        run: npm install\n\n      - name: Verify CDK version\n        working-directory: server/infra\n        run: npx cdk --version\n\n      - name: Configure AWS credentials (OIDC)\n        uses: aws-actions/configure-aws-credentials@v4\n        with:\n          role-to-assume: arn:aws:iam::287641434880:role/ItoGitHubCiCdRole\n          aws-region: us-west-2\n\n      - name: Synthesize CDK template\n        working-directory: server/infra\n        run: npx cdk synth\n\n      - name: Show CDK diff\n        working-directory: server/infra\n        run: npx cdk diff\n\n      - name: Refresh AWS credentials before deploy\n        uses: aws-actions/configure-aws-credentials@v4\n        with:\n          role-to-assume: arn:aws:iam::287641434880:role/ItoGitHubCiCdRole\n          aws-region: us-west-2\n\n      - name: Deploy to AWS\n        working-directory: server/infra\n        env:\n          AUTH0_DOMAIN: '${{ secrets.VITE_AUTH0_DOMAIN }}'\n          AUTH0_AUDIENCE: '${{ secrets.VITE_AUTH0_AUDIENCE }}'\n          AUTH0_CLIENT_ID: '${{ secrets.VITE_AUTH0_CLIENT_ID }}'\n          AUTH0_MGMT_CLIENT_ID: '${{ secrets.AUTH0_MGMT_CLIENT_ID }}'\n          AUTH0_MGMT_CLIENT_SECRET: '${{ secrets.AUTH0_MGMT_CLIENT_SECRET }}'\n          STRIPE_SECRET_KEY: '${{ secrets.STRIPE_SECRET_KEY }}'\n          STRIPE_WEBHOOK_SECRET: '${{ secrets.STRIPE_WEBHOOK_SECRET }}'\n          STRIPE_PRICE_ID: '${{ secrets.STRIPE_PRICE_ID }}'\n          APP_PROTOCOL: '${{ vars.APP_PROTOCOL }}'\n          STRIPE_PUBLIC_BASE_URL: '${{ vars.STRIPE_PUBLIC_BASE_URL }}'\n        run: npx cdk deploy \"Ito-${{ vars.AWS_STAGE }}/*\" --require-approval never\n"
  },
  {
    "path": ".github/workflows/native-build-check.yml",
    "content": "name: Native Build Check\n\non:\n  workflow_call:\n\njobs:\n  build-check-mac:\n    runs-on: macos-latest\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Set up Rust toolchain\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: stable\n          targets: 'x86_64-apple-darwin,aarch64-apple-darwin'\n\n      - name: Cache Rust dependencies\n        uses: actions/cache@v4\n        with:\n          path: |\n            ~/.cargo/bin/\n            ~/.cargo/registry/index/\n            ~/.cargo/registry/cache/\n            ~/.cargo/git/db/\n            native/target/\n          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-cargo-\n\n      - name: Verify native binaries compile (x86_64)\n        working-directory: native\n        run: cargo build --workspace --release --target x86_64-apple-darwin\n\n      - name: Verify native binaries compile (aarch64)\n        working-directory: native\n        run: cargo build --workspace --release --target aarch64-apple-darwin\n\n  build-check-windows:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Install build dependencies\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y mingw-w64\n\n      - name: Set up Rust toolchain\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: stable\n          targets: 'x86_64-pc-windows-gnu'\n\n      - name: Cache Rust dependencies\n        uses: actions/cache@v4\n        with:\n          path: |\n            ~/.cargo/bin/\n            ~/.cargo/registry/index/\n            ~/.cargo/registry/cache/\n            ~/.cargo/git/db/\n            native/target/\n          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-cargo-\n\n      - name: Verify native binaries compile (Windows)\n        working-directory: native\n        run: cargo build --workspace --release --target x86_64-pc-windows-gnu\n"
  },
  {
    "path": ".github/workflows/test-runner.yml",
    "content": "name: Test Runner\n\non:\n  workflow_call:\n\njobs:\n  test:\n    runs-on: macos-latest\n    env:\n      ITO_ENV: dev\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Set up Bun\n        uses: oven-sh/setup-bun@v1\n\n      - name: Set ITO_ENV based on branch\n        run: |\n          if [[ \"${GITHUB_REF_NAME}\" == \"main\" || \"${GITHUB_BASE_REF}\" == \"main\" ]]; then\n            echo \"ITO_ENV=prod\" >> \"$GITHUB_ENV\"\n          else\n            echo \"ITO_ENV=dev\" >> \"$GITHUB_ENV\"\n          fi\n\n      - name: Install dependencies\n        run: bun install && cd server && bun install && cd ..\n\n      - name: Run tests\n        run: bun runAllTests\n\n      - name: Check code formatting\n        run: bun format:app\n\n      - name: Check code linting\n        run: bun lint:app\n\n      - name: Check Rust formatting\n        run: bun format:native\n\n      - name: Check Rust linting\n        run: bun lint:native\n"
  },
  {
    "path": ".gitignore",
    "content": "out/*\nrelease/*\nnode_modules\n.env\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n*/dist/*\ndist/*\n.DS_Store\nnative/**/target\nnative/target-rust-analyzer\nnative/**/.build\nout.json\ntsconfig.node.tsbuildinfo\nserver/deploy-dev.sh\n\n# Sentry Config File\n.env.sentry-build-plugin\n.electron-builder-cache\n.npm-cache\n.cache\n"
  },
  {
    "path": ".prettierignore",
    "content": "node_modules/\ndist/\nbuild/\n.next/\ncoverage/\nout/\n\n# Generated proto files\n**/generated/\nlib/generated/\napp/generated/\nserver/src/generated/\n\n# CDK outputs\nserver/infra/cdk.out/\nserver/infra/*.d.ts\nserver/infra/*.js\n*.assets.json\n*.template.json\n\n# Build artifacts\n*.min.js\n*.bundle.js\n\n# Dependencies\n.pnpm-store/\nbun.lockb\npnpm-lock.yaml\nyarn.lock\npackage-lock.json\n\n# Test coverage\n.nyc_output/\n\n# IDE and system files\n.vscode/\n.idea/\n.DS_Store\n*.swp\n*.swo\n\n# Logs\n*.log\nlogs/\n\n# Environment files\n.env\n.env.*\n\n# Electron\n.vite/\n\n# Binary files\nresources/binaries/\n*.exe\n*.dll\n*.so\n*.dylib\n\n\n# Native build artifacts\nnative/**/.build/**\nnative/**/build/**\nnative/**/*.json"
  },
  {
    "path": ".prettierrc.json",
    "content": "{\n  \"semi\": false,\n  \"singleQuote\": true,\n  \"arrowParens\": \"avoid\",\n  \"tabWidth\": 2\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"eslint.validate\": [\n    \"javascript\",\n    \"javascriptreact\",\n    \"typescript\",\n    \"typescriptreact\"\n  ],\n  \"editor.formatOnSave\": true,\n  \"[javascript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[javascriptreact]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[typescript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[typescriptreact]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"rust-analyzer.linkedProjects\": [\n    \"./native/selected-text-reader/Cargo.toml\",\n    \"./native/text-writer/Cargo.toml\",\n    \"./native/audio-recorder/Cargo.toml\",\n    \"./native/active-application/Cargo.toml\",\n    \"./native/global-key-listener/Cargo.toml\"\n  ],\n  \"rust-analyzer.cargo.targetDir\": \"${workspaceFolder}/native/target-rust-analyzer\"\n}"
  },
  {
    "path": "AGENTS.md",
    "content": "# Repository Guidelines\n\n## Project Structure & Module Organization\n\n- `app/` hosts the Electron renderer (React + Tailwind); `lib/` contains shared TypeScript modules, preload logic, and unit tests.\n- `server/` holds the Bun-based transcription API, database migrations, and infrastructure scripts.\n- `native/` includes Rust crates for keyboard, audio, and text bridges; rebuild via scripts as needed.\n- `resources/` provides packaging assets (icons, updater configs), while `build/` and `out/` house generated installers; avoid editing build outputs.\n- Use `scripts/` for automation and keep generated proto/constants under `lib/generated` (created by build steps).\n\n## Build, Test, and Development Commands\n\n- `bun install` aligns dependencies after pulls.\n- `bun run dev` starts the Electron shell with live reload; `bun run dev:rust` rebuilds native bridges via `./build-binaries.sh` before launching.\n- Packaging: `bun run build:app` for cross-platform; use `bun run build:mac` / `bun run build:win` for platform installers.\n- Backend (`server/`): `bun install`, `bun run local-db-up` (Postgres), `bun run db:migrate`, `bun run dev`.\n\n## Coding Style & Naming Conventions\n\n- TypeScript + React across app/lib; server targets modern ECMAScript on Bun.\n- Prettier (2-space indent) and ESLint enforce formatting. Run `bun run format` and `bun run lint` (or `*:app` variants) before submitting.\n- Components/classes use `PascalCase`, hooks/utilities `camelCase`, constants `SCREAMING_SNAKE_CASE`. Co-locate Tailwind styles with components and reuse tokens via `lib/constants`.\n- Always prefer console commands over log commands. E.g. use `console.log` instead of `log.info`.\n\n## Testing Guidelines\n\n- Unit tests run with Bun. Renderer/shared specs live in `lib/__tests__`; server tests reside under `server/src/**`. Name files with `.test.ts`.\n- Run `bun run runLibTests`, `bun run runServerTests`, or `bun run runAllTests`; paste output in PR notes.\n- Seed backend tests with `bun run local-db-up` and avoid hitting external services—mock microphone, OS, and network dependencies.\n\n## Commit & Pull Request Guidelines\n\n- Conventional commits are enforced via commitlint (example: `feat(app): add dictation overlay`). Scope by top-level folder.\n- PRs need a summary, linked issue, test commands, and screenshots/GIFs for UI work. Call out schema/config updates and refresh `.env.example` files.\n\n## Environment & Security Notes\n\n- Copy `.env.example` (root) and `server/.env.example`; never commit credentials.\n- After modifying protobufs or constants, run `bun run generate:constants` so `lib/generated` stays in sync with the app bundle.\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# Claude Context for ITO Project\n\n## Project Overview\n\nThis is the ITO project - an AI assistant application with both client and server components.\n\n## Project Structure\n\n- `app/` - Client application code\n- `server/` - Server-side code with gRPC services\n- `server/src/ito.proto` - Protocol buffer definitions\n- `server/src/clients/` - Various client implementations (Groq, LLM providers, etc.)\n\n## Branch\n\nMain development branch: `dev`\n\n## Development Commands\n\n- Dev: `bun dev` (starts electron-vite dev with watch)\n- Server: `docker compose up --build` (run from server directory)\n- Build: `bun build:app:mac` or `bun build:app:windows`\n- Test: `bun runAllTests` (runs lib, server, and native tests)\n  - Lib tests: `bun runLibTests`\n  - Server tests: `bun runServerTests`\n  - Native tests: `bun runNativeTests` (or see \"Native Binary Tests\" section)\n- Lint:\n  - TypeScript: `bun lint` (check) or `bun lint:fix` (fix)\n  - Rust: `bun lint:native` (check) or `bun lint:fix:native` (fix)\n- Type check: `bun type-check`\n- Format:\n  - TypeScript: `bun format` (check) or `bun format:fix` (fix)\n  - Rust: `bun format:native` (check) or `bun format:fix:native` (fix)\n\n## Native Binary Tests\n\nThe `native/` directory contains Rust binaries that power the app's core functionality. The modules are organized as a Cargo workspace, allowing you to test and build all modules with a single command.\n\n### Running Tests\n\nTest all native modules:\n\n```bash\ncd native\ncargo test --workspace\n```\n\nOr use the npm script:\n\n```bash\nbun runNativeTests\n```\n\nTest a single module:\n\n```bash\ncd native/global-key-listener\ncargo test\n```\n\n### Native Modules\n\n- `global-key-listener` - Keyboard event capture and hotkey management\n- `audio-recorder` - Audio recording with sample rate conversion\n- `text-writer` - Cross-platform text input simulation\n- `active-application` - Active window detection\n- `selected-text-reader` - Selected text extraction\n\n### Linting and Formatting\n\nRust code follows standard formatting and linting rules defined in `native/`:\n\n- **rustfmt.toml** - Code formatting configuration (100 char width, Unix line endings)\n- **clippy.toml** - Linter configuration (cognitive complexity threshold)\n- **Cargo.toml** - Workspace-level lint rules (pedantic + nursery warnings)\n\nRun checks locally:\n\n```bash\n# Check formatting\nbun format:native\n\n# Auto-fix formatting\nbun format:fix:native\n\n# Check lints\nbun lint:native\n\n# Auto-fix lints (where possible)\nbun lint:fix:native\n```\n\n### CI/CD\n\nNative tests and builds are integrated into the existing CI workflows:\n\n**Tests** (`.github/workflows/test-runner.yml`):\n\n- Unit tests run on macOS runner (OS-agnostic tests)\n- Runs automatically via `bun runAllTests` on all pushes and PRs\n- Executed as part of the main CI controller workflow\n\n**Compilation Checks** (`.github/workflows/native-build-check.yml`):\n\n- macOS: Verifies compilation for x86_64 and aarch64 architectures\n- Windows: Verifies cross-compilation for x86_64-pc-windows-gnu\n- Runs automatically on all pushes and PRs via the CI controller\n- Ensures binaries compile correctly for both platforms before merging\n\n**Release Builds** (`.github/workflows/build.yml`):\n\n- Full release compilation happens during tagged releases\n- Also includes compilation verification before packaging\n\n## Code Style Preferences\n\n- Keep code as simple as possible\n- Don't create overly long files\n- Group related code into useful, well-named functions\n- Prefer clean, readable code over complex solutions\n- Follow existing patterns and conventions in the codebase\n- Always prefer console commands over log commands. E.g. use `console.log` instead of `log.info`.\n\n## Tech Stack\n\n- TypeScript\n- bun\n- gRPC with Protocol Buffers\n- React (for UI components)\n- Various LLM providers (Groq, etc.)\n"
  },
  {
    "path": "LICENSE",
    "content": "GNU GENERAL PUBLIC LICENSE\nVersion 3, 29 June 2007\n\nCopyright (C) 2024 Demox Labs\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>."
  },
  {
    "path": "README.md",
    "content": "# [DEPRECATED] - This project is no longer maintained \n\n# Ito\n\n<div align=\"center\">\n  <img src=\"resources/build/icon.png\" width=\"128\" height=\"128\" alt=\"Ito Logo\" />\n  \n  <h3>Smart dictation. Everywhere you want.</h3>\n  \n  <p>\n    <strong>Ito</strong> is an intelligent voice assistant that brings seamless voice dictation to any application on your computer. Simply hold down your trigger key, speak naturally, and watch your words appear instantly in any text field.\n  </p>\n\n  <p>\n    <img alt=\"macOS\" src=\"https://img.shields.io/badge/macOS-supported-blue?logo=apple&logoColor=white\">\n    <img alt=\"Windows\" src=\"https://img.shields.io/badge/Windows-supported-blue?logo=windows&logoColor=white\">\n    <img alt=\"Version\" src=\"https://img.shields.io/badge/version-0.2.0-green\">\n    <img alt=\"License\" src=\"https://img.shields.io/badge/license-GPL-blue\">\n  </p>\n</div>\n\n---\n\n## ✨ Features\n\n### 🎙️ **Universal Voice Dictation**\n\n- **Works in any app**: Emails, documents, chat applications, web browsers, code editors\n- **Global keyboard shortcuts**: Customizable trigger keys that work system-wide\n- **Real-time transcription**: High-accuracy speech-to-text powered by advanced AI models\n- **Instant text insertion**: Automatically types transcribed text into the focused text field\n\n### 🧠 **Smart & Adaptive**\n\n- **Custom dictionary**: Add technical terms, names, and specialized vocabulary\n- **Context awareness**: Learns from your usage patterns to improve accuracy\n- **Multi-language support**: Transcribe in multiple languages\n- **Intelligent punctuation**: Automatically adds appropriate punctuation\n\n### ⚙️ **Powerful Customization**\n\n- **Flexible shortcuts**: Configure any key combination as your trigger\n- **Audio preferences**: Choose your preferred microphone\n- **Privacy controls**: Local processing options and data control settings\n- **Seamless integration**: Works with any application\n\n### 💾 **Data Management**\n\n- **Notes system**: Automatically save transcriptions for later reference\n- **Interaction history**: Track your dictation sessions and improve over time\n- **Cloud sync**: Keep your settings and data synchronized across devices\n- **Export capabilities**: Export your notes and interaction data\n\n---\n\n## 🚀 Quick Start\n\n### Prerequisites\n\n- **macOS 10.15+** or **Windows 10+**\n- **Node.js 20+** and **Bun** (for development)\n- **Rust toolchain** (for building native components)\n- **Microphone access** and **Accessibility permissions**\n\n### Installation\n\n1. **Download the latest release** from [heyito.ai](https://www.heyito.ai/) or the [GitHub releases page](https://github.com/heyito/ito/releases)\n\n2. **Install the application**:\n   - **macOS**: Open the `.dmg` file and drag Ito to Applications\n   - **Windows**: Run the `.exe` installer and follow the setup wizard\n\n3. **Grant permissions** when prompted:\n   - **Microphone access**: Required for voice input\n   - **Accessibility access**: Required for global keyboard shortcuts and text insertion\n\n4. **Set up authentication**:\n   - Sign in with Google, Apple, Github through Auth0 or create a local account\n   - Complete the guided onboarding process\n\n### First Use\n\n1. **Configure your trigger key**: Choose a comfortable keyboard shortcut (default: `Fn + Space`)\n2. **Test your microphone**: Ensure clear audio input during the setup process\n3. **Try it out**: Hold your trigger key and speak into any text field\n4. **Customize settings**: Adjust voice sensitivity, shortcuts, and preferences\n\n---\n\n## 🛠️ Development\n\n### Building from Source\n\n> **Important**: Ito requires a local transcription server for voice processing. See [server/README.md](server/README.md) for detailed server setup instructions.\n\n```bash\n# Clone the repository\ngit clone https://github.com/heyito/ito.git\ncd ito\n\n# Install dependencies\nbun install\n\n# Set up environment variables\ncp .env.example .env\n\n# Build native components (Rust binaries)\n./build-binaries.sh\n\n# Set up and start the server (required for transcription)\ncd server\ncp .env.example .env  # Edit with your API keys\nbun install\nbun run local-db-up   # Start PostgreSQL database\nbun run db:migrate    # Run database migrations\nbun run dev           # Start development server\ncd ..\n\n# Start the Electron app (in a new terminal)\nbun run dev\n```\n\n### Build Requirements\n\n#### All Platforms\n\n- **Rust**: Install via [rustup.rs](https://rustup.rs/)\n  - **Windows users**: See Windows-specific instructions below for GNU toolchain setup\n  - **macOS/Linux users**: Default installation is sufficient\n\n#### macOS\n\n- **Xcode Command Line Tools**: `xcode-select --install`\n\n#### Windows\n\n**Required Setup:**\n\nThis setup uses git bash for shell operations. Download from [git](https://git-scm.com/downloads)\n\n1. **Install Docker Desktop**: Download from [docker.com](https://www.docker.com/products/docker-desktop/) and ensure it's running\n\n2. **Install Rust** (with GNU target)\n\nDownload and run the official [Rust installer for Windows](https://rustup.rs/).  \nThis installs `rustup` and the MSVC toolchain by default.\n\nAdd the GNU target (needed for our native components):\n\n    rustup toolchain install stable-x86_64-pc-windows-gnu\n    rustup target add x86_64-pc-windows-gnu\n\n---\n\n3. **Install 7-Zip**\n\n   winget install 7zip.7zip\n\n---\n\n4. **Install GCC & MinGW-w64 via MSYS2**\n\nInstall [MSYS2](https://www.msys2.org/).\n\nOpen the **MSYS2 MinGW x64** shell (from the Start Menu).\n\nUpdate and install the toolchain:\n\n    pacman -Syu       # run twice if asked to restart\n    pacman -S --needed mingw-w64-x86_64-toolchain\n\nVerify the tools exist:\n\n    ls /mingw64/bin/gcc.exe /mingw64/bin/dlltool.exe\n\n---\n\n5. **Use the MinGW tools when building** (Git Bash)\n\nYou normally develop and build in **Git Bash**. Before building, prepend the MinGW path:\n\n    export PATH=\"/c/msys64/mingw64/bin:$PATH\"\n    export DLLTOOL=\"/c/msys64/mingw64/bin/dlltool.exe\"\n    export CC_x86_64_pc_windows_gnu=\"/c/msys64/mingw64/bin/x86_64-w64-mingw32-gcc.exe\"\n    export AR_x86_64_pc_windows_gnu=\"/c/msys64/mingw64/bin/ar.exe\"\n    export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=\"/c/msys64/mingw64/bin/x86_64-w64-mingw32-gcc.exe\"\n\nCheck you’re picking up the right ones:\n\n    which gcc       # -> /c/msys64/mingw64/bin/gcc.exe\n    which dlltool   # -> /c/msys64/mingw64/bin/dlltool.exe\n\n⚠️ **Do not add `C:\\msys64\\ucrt64\\bin` to PATH.** That’s the wrong runtime and will break linking.\n\n💡 To avoid running these exports every session, add the lines above to your Git Bash `~/.bashrc` file. They will be applied automatically whenever you open a new Git Bash window.\n\n---\n\n6.  **Restart Git Bash if you update MSYS2**\n\nWhenever you update MSYS2 packages with `pacman -Syu`, restart Git Bash so the changes take effect.\n\n> **Note**: Windows builds use Docker for cross-compilation to ensure consistent builds. The Docker container handles the Windows build environment automatically.\n\n### Project Structure\n\n```\nito/\n├── app/                    # Electron renderer (React frontend)\n│   ├── components/         # React components\n│   ├── store/             # Zustand state management\n│   └── styles/            # TailwindCSS styles\n├── lib/                   # Shared library code\n│   ├── main/              # Electron main process\n│   ├── preload/           # Preload scripts & IPC\n│   └── media/             # Audio/keyboard native interfaces\n├── native/                # Native components (Rust/Swift)\n│   ├── audio-recorder/    # Audio capture (Rust)\n│   ├── global-key-listener/ # Keyboard events (Rust)\n│   ├── text-writer/       # Text insertion (Rust)\n│   └── active-application/ # Get the active application for context (Rust)\n├── server/                # gRPC transcription server\n│   ├── src/               # Server implementation\n│   └── infra/             # AWS infrastructure (CDK)\n└── resources/             # Build resources & assets\n```\n\n### Available Scripts\n\n```bash\n# Development\nbun run dev                 # Start with hot reload\nbun run dev:rust           # Build Rust components and start dev\n\n# Building Native Components\nbun run build:rust         # Build for current platform\nbun run build:rust:mac     # Build for macOS (with universal binary)\nbun run build:rust:win     # Build for Windows\n\n# Building Application\nbun run build:mac          # Build for macOS\nbun run build:win          # Build for Windows\n./build-app.sh mac          # Build macOS using build script\n./build-app.sh windows      # Build Windows using build script (requires Docker)\n\n# Code Quality\nbun run lint               # Run ESLint\nbun run format             # Run Prettier\nbun run lint:fix           # Fix linting issues\n```\n\n---\n\n## 🏗️ Architecture\n\n### Client Architecture\n\n**Ito** is built as a modern Electron application with a sophisticated multi-process architecture:\n\n- **Main Process**: Handles system integration, permissions, and native component coordination\n- **Renderer Process**: React-based UI with real-time audio visualization\n- **Preload Scripts**: Secure IPC bridge between main and renderer processes\n- **Native Components**: High-performance Rust binaries for audio capture and keyboard handling\n\n### Technology Stack\n\n**Frontend:**\n\n- **Electron** - Cross-platform desktop framework\n- **React 19** - Modern UI library with concurrent features\n- **TypeScript** - Type-safe development\n- **TailwindCSS** - Utility-first styling\n- **Zustand** - Lightweight state management\n- **Framer Motion** - Smooth animations\n\n**Backend:**\n\n- **Node.js** - Runtime environment\n- **gRPC** - High-performance RPC for transcription services\n- **SQLite** - Local data storage\n- **Protocol Buffers** - Efficient data serialization\n\n**Native Components:**\n\n- **Rust** - System-level audio recording and keyboard event handling\n- **Swift** - macOS-specific text manipulation and accessibility features\n- **cpal** - Cross-platform audio library\n- **enigo** - Cross-platform input simulation\n\n**Infrastructure:**\n\n- **AWS CDK** - Infrastructure as code\n- **Docker** - Containerized deployments\n- **Auth0** - Authentication and user management\n\n### Communication Flow\n\n```mermaid\ngraph TD\n    A[User Holds Trigger Key] --> B[Global Key Listener]\n    B --> C[Main Process]\n    C --> D[Audio Recorder Service]\n    D --> E[gRPC Transcription Service]\n    E --> F[AI Transcription Model]\n    F --> G[Transcribed Text]\n    G --> H[Text Writer Service]\n    H --> I[Active Text Field]\n```\n\n---\n\n## 🔧 Configuration\n\n### Keyboard Shortcuts\n\nCustomize your trigger keys in **Settings > Keyboard**:\n\n- **Single key**: `Space`, `Fn`, etc.\n- **Key combinations**: `Cmd + Space`, `Ctrl + Shift + V`, etc.\n- **Complex shortcuts**: `Fn + Cmd + Space` for advanced workflows\n\n### Audio Settings\n\nFine-tune audio capture in **Settings > Audio**:\n\n- **Microphone selection**: Choose from available input devices\n- **Sensitivity adjustment**: Optimize for your voice and environment\n- **Noise reduction**: Filter background noise automatically\n- **Audio feedback**: Enable/disable sound effects\n\n### Privacy & Data\n\nControl your data in **Settings > General**:\n\n- **Local processing**: Keep voice data on your device\n- **Cloud sync**: Synchronize settings across devices\n- **Analytics**: Share anonymous usage data (optional)\n- **Data export**: Download your notes and interaction history\n\n---\n\n## 🔒 Privacy & Security\n\n### Data Handling\n\n- **Local-enabled**: Voice processing can be done entirely on your device or using our cloud\n- **Encrypted transmission**: All network communication uses TLS encryption\n- **Minimal data collection**: Only essential data is processed and stored\n- **User control**: Full control and transparency over data retention and deletion\n\n### Permissions\n\n**Ito** requires specific system permissions to function:\n\n- **Microphone Access**: To capture your voice for transcription\n- **Accessibility Access**: To detect keyboard shortcuts and insert text\n- **Network Access**: For cloud features and updates (optional)\n\n### Open Source\n\nThis project is open source under the GNU General Public License. You can:\n\n- Audit the source code for security and privacy\n- Contribute improvements and bug fixes\n- Fork and customize for your specific needs\n- Report security issues through responsible disclosure\n\n---\n\n## 🤝 Contributing\n\nWe welcome contributions! Whether you're fixing bugs, adding features, or improving documentation, your help makes **Ito** better for everyone.\n\n### Getting Started\n\n1. **Fork the repository** and clone your fork\n2. **Create a feature branch** from `dev`\n3. **Make your changes** with clear commit messages\n4. **Test thoroughly** across supported platforms\n5. **Submit a pull request** with a detailed description\n\n### Development Guidelines\n\n- **Code Style**: Use Prettier and ESLint configurations\n- **Type Safety**: Maintain strong TypeScript typing\n- **Testing**: Add tests for new features\n- **Documentation**: Update docs for API changes\n- **Performance**: Consider impact on time between recording and text insertion\n\n### Areas for Contribution\n\n- **Accuracy improvements**: Better transcription algorithms\n- **Language support**: Additional language models\n- **UI/UX enhancements**: Better user experience\n- **Platform support**: Windows stability testing, Linux compatibility\n- **Documentation**: Tutorials, guides, and examples\n\n---\n\n## 📄 License\n\nThis project is licensed under the **GNU General Public License** - see the [LICENSE](LICENSE) file for details.\n\n---\n\n## 🙏 Acknowledgments\n\n**Ito** is built with and inspired by amazing open source projects:\n\n- **[Electron React App](https://github.com/guasam/electron-react-app)** by @guasam - The foundational template that provided our modern Electron + React architecture\n- **Electron** - Cross-platform desktop apps with web technologies\n- **React** - Modern UI development\n- **Rust** - Systems programming language for native components\n- **gRPC** - High-performance RPC framework\n- **TailwindCSS** - Utility-first CSS framework\n\n---\n\n## 📞 Support\n\n- **Community**: [GitHub Discussions](https://github.com/heyito/ito/discussions)\n- **Issues**: [GitHub Issues](https://github.com/heyito/ito/issues)\n- **Website**: [heyito.ai](https://www.heyito.ai)\n"
  },
  {
    "path": "app/app.tsx",
    "content": "import { HashRouter, Routes, Route } from 'react-router-dom'\nimport appIcon from '@/resources/build/icon.png'\nimport HomeKit from '@/app/components/home/HomeKit'\nimport WelcomeKit from '@/app/components/welcome/WelcomeKit'\nimport Pill from '@/app/components/pill/Pill'\nimport {\n  STEP_NAMES,\n  STEP_NAMES_ARRAY,\n  useOnboardingStore,\n} from '@/app/store/useOnboardingStore'\nimport { useAuth } from '@/app/components/auth/useAuth'\nimport { WindowContextProvider } from '@/lib/window'\nimport { Auth0Provider } from '@/app/components/auth/Auth0Provider'\nimport { useDeviceChangeListener } from './hooks/useDeviceChangeListener'\nimport { verifyStoredMicrophone } from './media/microphone'\nimport { useEffect } from 'react'\n\nconst MainApp = () => {\n  const { onboardingCompleted, onboardingStep } = useOnboardingStore()\n  const { isAuthenticated } = useAuth()\n  useDeviceChangeListener()\n\n  useEffect(() => {\n    verifyStoredMicrophone()\n  }, [])\n\n  const onboardingSetupCompleted =\n    onboardingStep >= STEP_NAMES_ARRAY.indexOf(STEP_NAMES.TRY_IT_OUT)\n\n  const shouldEnableShortcutGlobally =\n    onboardingCompleted || onboardingSetupCompleted\n\n  // If authenticated and onboarding completed, show main app\n  if (isAuthenticated && onboardingCompleted) {\n    window.api.send(\n      'electron-store-set',\n      'settings.isShortcutGloballyEnabled',\n      shouldEnableShortcutGlobally,\n    )\n    return <HomeKit />\n  }\n\n  // If authenticated but onboarding not completed, continue onboarding\n  window.api.send(\n    'electron-store-set',\n    'settings.isShortcutGloballyEnabled',\n    shouldEnableShortcutGlobally,\n  )\n  return <WelcomeKit />\n}\n\nexport default function App() {\n  return (\n    <Auth0Provider>\n      <HashRouter>\n        <Routes>\n          {/* Route for the pill window */}\n          <Route\n            path=\"/pill\"\n            element={\n              <>\n                <Pill />\n              </>\n            }\n          />\n\n          {/* Default route for the main application window */}\n          <Route\n            path=\"/\"\n            element={\n              <>\n                <WindowContextProvider\n                  titlebar={{ title: 'Ito', icon: appIcon }}\n                >\n                  <MainApp />\n                </WindowContextProvider>\n              </>\n            }\n          />\n        </Routes>\n      </HashRouter>\n    </Auth0Provider>\n  )\n}\n"
  },
  {
    "path": "app/assets/.gitignore",
    "content": ""
  },
  {
    "path": "app/components/analytics/index.ts",
    "content": "import posthog from 'posthog-js'\nimport log from 'electron-log'\nimport { STORE_KEYS } from '../../../lib/constants/store-keys'\nimport { v4 as uuidv4 } from 'uuid'\nimport type { OnboardingCategory } from '../../store/useOnboardingStore'\n\n// Get or generate a machine-based device ID that's shared across all windows\nconst getSharedDeviceId = async (): Promise<string> => {\n  try {\n    // Just request the device ID - main process handles generation/caching\n    const deviceId = await window.api?.invoke('analytics:get-device-id')\n    if (deviceId) {\n      console.log('[Analytics] Using machine-based device ID:', deviceId)\n      return deviceId\n    }\n    throw new Error('No device ID returned from main process')\n  } catch (error) {\n    log.error('[Analytics] Could not get machine device ID:', error)\n    // In true emergency, generate a temporary UUID as fallback\n    return uuidv4()\n  }\n}\n\n// Check if analytics should be enabled\nconst getAnalyticsEnabled = (): boolean => {\n  if (!import.meta.env.VITE_POSTHOG_API_KEY) {\n    console.warn('[Analytics] No PostHog API key found, analytics disabled')\n    return false\n  }\n  try {\n    const settings = window.electron?.store?.get(STORE_KEYS.SETTINGS)\n    return settings?.shareAnalytics ?? true\n  } catch (error) {\n    console.warn(\n      '[Analytics] Could not read settings, defaulting to enabled:',\n      error,\n    )\n    return true\n  }\n}\n\nconst initPostHog = () => {\n  const isPill =\n    typeof window !== 'undefined' &&\n    typeof window.location !== 'undefined' &&\n    typeof window.location.hash === 'string' &&\n    window.location.hash.startsWith('#/pill')\n\n  posthog.init(import.meta.env.VITE_POSTHOG_API_KEY, {\n    api_host: import.meta.env.VITE_POSTHOG_HOST,\n    disable_session_recording: true,\n    disable_surveys: true,\n    advanced_disable_decide: true,\n    persistence: 'cookie',\n    // Disable default web auto-capture and pageviews for the pill window only\n    autocapture: !isPill,\n    capture_pageview: !isPill,\n    sanitize_properties: (props: Record<string, unknown>) => {\n      const p = { ...props }\n      delete (p as any).$current_url\n      delete (p as any).$pathname\n      delete (p as any).$host\n      delete (p as any).$referrer\n      return p\n    },\n  })\n}\n\n// Initialize PostHog only if analytics is enabled\nlet isAnalyticsInitialized = false\nlet sharedDeviceId: string | null = null\nconst analyticsEnabled = getAnalyticsEnabled()\n\nconsole.log('[Analytics] Analytics enabled:', analyticsEnabled)\n\n// Initialize PostHog asynchronously\nconst initializeAnalytics = async () => {\n  if (!analyticsEnabled) {\n    console.log('[Analytics] PostHog disabled by user settings')\n    return\n  }\n\n  try {\n    sharedDeviceId = await getSharedDeviceId()\n    console.log('[Analytics] Using shared device ID:', sharedDeviceId)\n\n    initPostHog()\n\n    if (sharedDeviceId) {\n      posthog.register({ device_id: sharedDeviceId })\n    }\n    // Attempt to resolve and alias install token to website distinct id\n    try {\n      const result = await window.api?.invoke('analytics:resolve-install-token')\n      if (result && result.success && result.websiteDistinctId) {\n        try {\n          posthog.alias(result.websiteDistinctId)\n          console.log(\n            '[Analytics] Aliased to website distinct_id from install token',\n          )\n        } catch (aliasErr) {\n          log.warn('[Analytics] alias() failed:', aliasErr)\n        }\n      }\n    } catch (err) {\n      log.warn('[Analytics] resolve-install-token failed:', err)\n    }\n    isAnalyticsInitialized = true\n\n    // Update the service instance after successful initialization\n    analytics.updateInitializationStatus(true, sharedDeviceId)\n\n    console.log(\n      '[Analytics] PostHog initialized with shared device ID:',\n      sharedDeviceId,\n    )\n  } catch (error) {\n    log.error('[Analytics] Failed to initialize analytics:', error)\n  }\n}\n\n// Initialize analytics when the module loads\ninitializeAnalytics()\n\n// Event types for type safety\nexport interface BaseEventProperties {\n  timestamp?: string\n  session_id?: string\n  [key: string]: any\n}\n\nexport interface OnboardingEventProperties extends BaseEventProperties {\n  step: number\n  step_name: string\n  category: OnboardingCategory\n  total_steps: number\n  referral_source?: string\n  provider?: string\n}\n\nexport interface HotkeyEventProperties extends BaseEventProperties {\n  action: 'press' | 'release'\n  keys: string[]\n  duration_ms?: number\n  session_duration_ms?: number\n}\n\nexport interface AuthEventProperties extends BaseEventProperties {\n  provider: string\n  is_returning_user: boolean\n  user_id?: string\n}\n\nexport interface SettingsEventProperties extends BaseEventProperties {\n  setting_name: string\n  old_value: any\n  new_value: any\n  setting_category: string\n}\n\nexport interface UserProperties {\n  user_id: string\n  email?: string\n  name?: string\n  provider?: string\n  created_at?: string\n  last_active?: string\n  onboarding_completed?: boolean\n  referral_source?: string\n  keyboard_shortcuts?: string[]\n}\n\n// Event constants\nexport const ANALYTICS_EVENTS = {\n  // Onboarding events\n  ONBOARDING_STARTED: 'onboarding_started',\n  ONBOARDING_STEP_COMPLETED: 'onboarding_step_completed',\n  ONBOARDING_STEP_VIEWED: 'onboarding_step_viewed',\n  ONBOARDING_COMPLETED: 'onboarding_completed',\n  ONBOARDING_ABANDONED: 'onboarding_abandoned',\n\n  // Authentication events\n  AUTH_SIGNUP_STARTED: 'auth_signup_started',\n  AUTH_SIGNUP_COMPLETED: 'auth_signup_completed',\n  AUTH_SIGNIN_STARTED: 'auth_signin_started',\n  AUTH_SIGNIN_COMPLETED: 'auth_signin_completed',\n  AUTH_SIGNIN_FAILED: 'auth_signin_failed',\n  AUTH_LOGOUT: 'auth_logout',\n  AUTH_LOGOUT_FAILED: 'auth_logout_failed',\n  AUTH_STATE_GENERATION_FAILED: 'auth_state_generation_failed',\n  AUTH_METHOD_FAILED: 'auth_method_failed',\n\n  // Recording events\n  RECORDING_STARTED: 'recording_started',\n  RECORDING_COMPLETED: 'recording_completed',\n  MANUAL_RECORDING_STARTED: 'manual_recording_started',\n  MANUAL_RECORDING_COMPLETED: 'manual_recording_completed',\n  MANUAL_RECORDING_ABANDONED: 'manual_recording_abandoned',\n\n  // Settings events\n  SETTING_CHANGED: 'setting_changed',\n  MICROPHONE_CHANGED: 'microphone_changed',\n  KEYBOARD_SHORTCUTS_CHANGED: 'keyboard_shortcuts_changed',\n} as const\n\nexport type AnalyticsEvent =\n  (typeof ANALYTICS_EVENTS)[keyof typeof ANALYTICS_EVENTS]\n\n/**\n * Professional Analytics Service for Ito\n * Handles all analytics tracking with proper typing and error handling\n */\nclass AnalyticsService {\n  private isInitialized: boolean = isAnalyticsInitialized\n  private currentUserId: string | null = null\n  private currentProvider: string | null = null\n  private sessionStartTime: number = Date.now()\n  private deviceId: string | null = null\n\n  constructor() {\n    // Device ID will be set after async initialization\n    this.deviceId = sharedDeviceId\n    console.log(\n      `[Analytics] Service initialized (enabled: ${this.isInitialized}, deviceId: ${this.deviceId || 'pending'})`,\n    )\n  }\n\n  /**\n   * Enable analytics (re-initialize if needed)\n   */\n  async enableAnalytics() {\n    if (!this.isInitialized && import.meta.env.VITE_POSTHOG_API_KEY) {\n      try {\n        const deviceId = await getSharedDeviceId()\n        this.deviceId = deviceId\n        initPostHog()\n        if (deviceId) {\n          posthog.register({ device_id: deviceId })\n        }\n        this.isInitialized = true\n        console.log(\n          '[Analytics] Analytics enabled and initialized with shared device ID:',\n          deviceId,\n        )\n      } catch (error) {\n        log.error('[Analytics] Failed to enable analytics:', error)\n      }\n    }\n  }\n\n  /**\n   * Disable analytics\n   */\n  disableAnalytics() {\n    this.isInitialized = false\n    this.currentUserId = null\n    this.currentProvider = null\n    try {\n      posthog.opt_out_capturing()\n    } catch (error) {\n      log.warn('[Analytics] Failed to opt-out capturing:', error)\n    }\n    console.log('[Analytics] Analytics disabled')\n  }\n\n  /**\n   * Check if analytics is currently enabled\n   */\n  isEnabled(): boolean {\n    return this.isInitialized\n  }\n\n  /**\n   * Set user identification and properties\n   */\n  identifyUser(\n    userId: string,\n    properties: Partial<UserProperties> = {},\n    provider?: string,\n  ) {\n    console.log('identifyUser', userId, properties, provider)\n\n    // Store provider information\n    if (provider) {\n      this.currentProvider = provider\n    }\n\n    if (!this.shouldTrack()) {\n      console.log(\n        '[Analytics] User identification skipped - analytics disabled or self-hosted user',\n      )\n      return\n    }\n\n    try {\n      if (this.currentUserId !== userId) {\n        this.currentUserId = userId\n        const props = {\n          user_id: userId,\n          last_active: new Date().toISOString(),\n          ...properties,\n        }\n        posthog.identify(userId, props)\n        console.log(\n          `[Analytics] User identified: ${userId} (deviceId: ${this.deviceId || 'pending'})`,\n        )\n      } else if (Object.keys(properties).length > 0) {\n        posthog.identify(undefined, {\n          ...properties,\n          last_active: new Date().toISOString(),\n        })\n      }\n    } catch (error) {\n      log.error('[Analytics] Failed to identify user:', error)\n    }\n  }\n\n  /**\n   * Update user properties\n   */\n  updateUserProperties(properties: Partial<UserProperties>) {\n    if (!this.shouldTrack() || !this.currentUserId) {\n      console.log(\n        '[Analytics] User properties update skipped - analytics disabled, self-hosted user, or user not identified',\n      )\n      return\n    }\n\n    try {\n      posthog.identify(undefined, properties)\n      console.log('[Analytics] User properties updated')\n    } catch (error) {\n      log.error('[Analytics] Failed to update user properties:', error)\n    }\n  }\n\n  /**\n   * Track a generic event\n   */\n  track(eventName: AnalyticsEvent, properties: BaseEventProperties = {}) {\n    if (!this.shouldTrack()) {\n      return\n    }\n\n    try {\n      const eventProperties = {\n        timestamp: new Date().toISOString(),\n        session_duration_ms: Date.now() - this.sessionStartTime,\n        ...properties,\n      }\n\n      posthog.capture(eventName, {\n        ...eventProperties,\n        ...(this.currentUserId ? { user_id: this.currentUserId } : {}),\n      })\n      console.log(\n        `[Analytics] Event tracked: ${eventName} (deviceId: ${this.deviceId || 'pending'}, userId: ${this.currentUserId || 'anonymous'})`,\n      )\n    } catch (error) {\n      log.error(`[Analytics] Failed to track event ${eventName}:`, error)\n    }\n  }\n\n  /**\n   * Track onboarding events\n   */\n  trackOnboarding(\n    eventName: Extract<\n      AnalyticsEvent,\n      | 'onboarding_started'\n      | 'onboarding_step_completed'\n      | 'onboarding_step_viewed'\n      | 'onboarding_completed'\n      | 'onboarding_abandoned'\n    >,\n    properties: OnboardingEventProperties,\n  ) {\n    console.log('trackOnboarding', eventName, properties)\n    this.track(eventName, properties)\n  }\n\n  /**\n   * Track authentication events\n   */\n  trackAuth(\n    eventName: Extract<\n      AnalyticsEvent,\n      | 'auth_signup_started'\n      | 'auth_signup_completed'\n      | 'auth_signin_started'\n      | 'auth_signin_completed'\n      | 'auth_logout'\n    >,\n    properties: AuthEventProperties,\n  ) {\n    this.track(eventName, properties)\n  }\n\n  /**\n   * Track settings changes\n   */\n  trackSettings(\n    eventName: Extract<\n      AnalyticsEvent,\n      | 'setting_changed'\n      | 'microphone_changed'\n      | 'keyboard_shortcut_changed'\n      | 'privacy_mode_toggled'\n      | 'keyboard_shortcuts_changed'\n    >,\n    properties: SettingsEventProperties,\n  ) {\n    this.track(eventName, properties)\n  }\n\n  /**\n   * Track permission events\n   */\n  trackPermission(\n    eventName: Extract<\n      AnalyticsEvent,\n      'permission_requested' | 'permission_granted' | 'permission_denied'\n    >,\n    permissionType: 'microphone' | 'accessibility',\n    properties: BaseEventProperties = {},\n  ) {\n    this.track(eventName, {\n      permission_type: permissionType,\n      ...properties,\n    })\n  }\n\n  /**\n   * Reset analytics (for logout)\n   */\n  resetUser() {\n    if (!this.isInitialized) {\n      console.log('[Analytics] User reset skipped - analytics disabled')\n      return\n    }\n\n    try {\n      posthog.reset()\n      this.currentUserId = null\n      this.currentProvider = null\n      console.log('[Analytics] User session reset')\n    } catch (error) {\n      log.error('[Analytics] Failed to reset user session:', error)\n    }\n  }\n\n  /**\n   * Get current session duration\n   */\n  getSessionDuration(): number {\n    return Date.now() - this.sessionStartTime\n  }\n\n  /**\n   * Check if user is identified\n   */\n  isUserIdentified(): boolean {\n    return this.currentUserId !== null\n  }\n\n  /**\n   * Get the current device ID\n   */\n  getDeviceId(): string | null {\n    return this.deviceId\n  }\n\n  /**\n   * Update initialization status (called after async initialization completes)\n   */\n  updateInitializationStatus(isInitialized: boolean, deviceId: string | null) {\n    this.isInitialized = isInitialized\n    this.deviceId = deviceId\n    console.log(\n      `[Analytics] Service status updated (enabled: ${this.isInitialized}, deviceId: ${this.deviceId})`,\n    )\n  }\n\n  /**\n   * Check if analytics should be tracked based on provider\n   */\n  private shouldTrack(): boolean {\n    if (!this.isInitialized) {\n      return false\n    }\n\n    // Skip tracking for self-hosted users\n    if (this.currentProvider === 'self-hosted') {\n      console.log('[Analytics] Tracking skipped - self-hosted user')\n      return false\n    }\n\n    return true\n  }\n}\n\n// Export singleton instance\nexport const analytics = new AnalyticsService()\n\n// Function to update analytics based on settings change\nexport const updateAnalyticsFromSettings = (shareAnalytics: boolean) => {\n  if (shareAnalytics && !analytics.isEnabled()) {\n    analytics.enableAnalytics()\n    console.log('[Analytics] Analytics enabled by settings change')\n  } else if (!shareAnalytics && analytics.isEnabled()) {\n    analytics.disableAnalytics()\n    console.log('[Analytics] Analytics disabled by settings change')\n  }\n}\n\n// Export convenience functions\nexport const trackEvent = analytics.track.bind(analytics)\nexport const identifyUser = analytics.identifyUser.bind(analytics)\nexport const updateUserProperties =\n  analytics.updateUserProperties.bind(analytics)\nexport const resetAnalytics = analytics.resetUser.bind(analytics)\n"
  },
  {
    "path": "app/components/auth/Auth0Provider.tsx",
    "content": "import React from 'react'\nimport { Auth0Provider as Auth0ReactProvider } from '@auth0/auth0-react'\nimport { Auth0Config, validateAuth0Config } from '../../../lib/auth/config'\n\ninterface Auth0ProviderProps {\n  children: React.ReactNode\n}\n\nexport const Auth0Provider: React.FC<Auth0ProviderProps> = ({ children }) => {\n  // Validate configuration on startup\n  React.useEffect(() => {\n    try {\n      validateAuth0Config()\n    } catch (error) {\n      console.error('Auth0 configuration error:', error)\n    }\n  }, [])\n\n  return (\n    <Auth0ReactProvider\n      domain={Auth0Config.domain}\n      clientId={Auth0Config.clientId}\n      authorizationParams={{\n        redirect_uri: Auth0Config.redirectUri,\n        scope: Auth0Config.scope,\n      }}\n      useRefreshTokens={Auth0Config.useRefreshTokens}\n      cacheLocation={Auth0Config.cacheLocation}\n    >\n      {children}\n    </Auth0ReactProvider>\n  )\n}\n\nexport default Auth0Provider\n"
  },
  {
    "path": "app/components/auth/useAuth.ts",
    "content": "import { useAuth0 } from '@auth0/auth0-react'\nimport { useCallback, useEffect, useMemo } from 'react'\nimport { Auth0Connections, Auth0Config } from '../../../lib/auth/config'\nimport { useAuthStore } from '../../store/useAuthStore'\nimport { type AuthUser, type AuthTokens } from '../../../lib/main/store'\nimport { useMainStore } from '@/app/store/useMainStore'\nimport { analytics, ANALYTICS_EVENTS } from '../analytics'\nimport { STORE_KEYS } from '../../../lib/constants/store-keys'\nimport { useOnboardingStore } from '@/app/store/useOnboardingStore'\n\nexport const useAuth = () => {\n  const {\n    logout,\n    user,\n    isAuthenticated: auth0IsAuthenticated,\n    isLoading: auth0IsLoading,\n    error: auth0Error,\n    getAccessTokenSilently,\n    getIdTokenClaims,\n  } = useAuth0()\n\n  // Get auth state from our store\n  const {\n    isAuthenticated: storeIsAuthenticated,\n    user: storeUser,\n    tokens,\n    isLoading: storeIsLoading,\n    error: storeError,\n    clearAuth,\n    setSelfHostedMode,\n  } = useAuthStore()\n\n  // Combine Auth0 and store state - prioritize store state for external auth\n  const isAuthenticated = storeIsAuthenticated || auth0IsAuthenticated\n  const isLoading = storeIsLoading || auth0IsLoading\n  const error = storeError || auth0Error\n\n  // Convert Auth0 user to our user interface\n  const auth0User: AuthUser | null = useMemo(() => {\n    if (!user) return null\n    return {\n      id: user.sub || '',\n      email: user.email,\n      name: user.name,\n      picture: user.picture,\n      provider: user.sub?.includes('|') ? user.sub.split('|')[0] : 'unknown',\n      lastSignInAt: new Date().toISOString(), // Only updated when user object changes\n    }\n  }, [user]) // Dependency array now correctly includes 'user'\n\n  // Prioritize store user over Auth0 user\n  const authUser = storeUser || auth0User\n\n  // Hydrate per-user onboarding state helper\n  async function hydrateOnboardingState(context?: string): Promise<void> {\n    try {\n      const saved = await window.api.getOnboardingState?.()\n      const onboarding = useOnboardingStore.getState()\n      if (saved?.onboardingCompleted) {\n        onboarding.setOnboardingCompleted()\n      } else {\n        onboarding.resetOnboarding()\n        onboarding.incrementOnboardingStep()\n      }\n    } catch (e) {\n      const suffix = context ? ` (${context})` : ''\n      console.warn(`[useAuth] Failed to hydrate onboarding state${suffix}:`, e)\n    }\n  }\n\n  // Check for token expiration on startup\n  useEffect(() => {\n    // Check if we have valid auth state stored\n    const storedAuth = window.electron?.store?.get(STORE_KEYS.AUTH)\n    const hasStoredTokens = storedAuth?.tokens?.access_token\n    // Also check main store for tokens (backwards compatibility)\n    const hasMainStoreToken = window.electron?.store?.get(\n      STORE_KEYS.ACCESS_TOKEN,\n    )\n\n    if ((hasStoredTokens || hasMainStoreToken) && !isAuthenticated) {\n      console.log('Detected expired tokens on startup, clearing auth state')\n\n      // Clear any remaining auth data\n      clearAuth(true)\n\n      // Track the automatic logout\n      const currentUser = authUser\n      if (currentUser) {\n        analytics.trackAuth(ANALYTICS_EVENTS.AUTH_LOGOUT, {\n          provider: currentUser.provider || 'unknown',\n          is_returning_user: true,\n          user_id: currentUser.id,\n          complete_signout: false,\n          session_duration_ms: analytics.getSessionDuration(),\n          reason: 'token_expired_startup',\n        })\n      }\n    }\n  }, [isAuthenticated, authUser, clearAuth])\n\n  useEffect(() => {\n    if (authUser) {\n      analytics.identifyUser(\n        authUser.id,\n        {\n          user_id: authUser.id,\n          email: authUser.email,\n          name: authUser.name,\n          provider: authUser.provider,\n          created_at: authUser.lastSignInAt,\n        },\n        authUser.provider,\n      )\n\n      // Notify pill window of user authentication\n      if (window.api?.notifyUserAuthUpdate) {\n        window.api.notifyUserAuthUpdate({\n          id: authUser.id,\n          email: authUser.email,\n          name: authUser.name,\n          provider: authUser.provider,\n        })\n      }\n    } else {\n      // Notify pill window that user is not authenticated (logout/reset)\n      if (window.api?.notifyUserAuthUpdate) {\n        console.log('[useAuth] Notifying pill window of user reset')\n        window.api.notifyUserAuthUpdate(null)\n      }\n    }\n  }, [\n    authUser,\n    auth0IsAuthenticated,\n    auth0User,\n    storeIsAuthenticated,\n    storeUser,\n  ])\n\n  // Handle auth code from protocol URL - only set up listener once globally\n  useEffect(() => {\n    if (!window.api?.on) {\n      console.warn('window.api.on not available')\n      return\n    }\n\n    // Check if listener is already set up\n    if ((window as any).__authCodeListenerSetup) {\n      return\n    }\n\n    // Mark that we've set up the listener\n    ;(window as any).__authCodeListenerSetup = true\n\n    const cleanup = window.api.on(\n      'auth-code-received',\n      async (authCode: string, state: string) => {\n        try {\n          // Exchange authorization code for tokens via main process\n          const result = await window.api.invoke('exchange-auth-code', {\n            authCode,\n            state,\n            config: Auth0Config,\n          })\n\n          if (!result.success) {\n            throw new Error(result.error)\n          }\n\n          // Store tokens and user info in the auth store\n          if (result.tokens && result.userInfo) {\n            // Extract provider from Auth0 user ID (format: \"provider|id\")\n            const providerId = result.userInfo.id || ''\n            const provider = providerId.includes('|')\n              ? providerId.split('|')[0]\n              : 'unknown'\n\n            // Check if this is a returning user\n            const existingUser = useAuthStore.getState().user\n            const isReturningUser =\n              !!existingUser && existingUser.id === result.userInfo.id\n\n            useAuthStore\n              .getState()\n              .setAuthData(\n                result.tokens as AuthTokens,\n                result.userInfo as AuthUser,\n                provider,\n              )\n\n            // Track successful signin\n            analytics.trackAuth(ANALYTICS_EVENTS.AUTH_SIGNIN_COMPLETED, {\n              provider,\n              is_returning_user: isReturningUser,\n              user_id: result.userInfo.id,\n            })\n\n            useMainStore.getState().setCurrentPage('home')\n\n            await window.api.notifyLoginSuccess(\n              result.userInfo,\n              result.tokens.id_token,\n              result.tokens.access_token,\n            )\n\n            // Hydrate per-user onboarding state from SQLite for the now-authenticated user\n            await hydrateOnboardingState()\n          } else {\n            throw new Error('Missing tokens or user info in response')\n          }\n        } catch (error) {\n          console.error('Error handling auth code from protocol URL:', error)\n\n          // Track signin failure\n          analytics.track(ANALYTICS_EVENTS.AUTH_SIGNIN_FAILED, {\n            error_message:\n              error instanceof Error ? error.message : 'Unknown error',\n            auth_method: 'external_browser',\n          })\n\n          alert(\n            `Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`,\n          )\n        }\n      },\n    )\n\n    return () => {\n      cleanup()\n      ;(window as any).__authCodeListenerSetup = false\n    }\n  }, [])\n\n  // External browser authentication - now the primary method\n  const openExternalAuth = useCallback(\n    async (\n      connection?: string,\n      options?: { email?: string; mode?: 'login' | 'signup' },\n    ) => {\n      // Track signin attempt started\n      const provider = connection || 'unknown'\n      const eventType =\n        options?.mode === 'signup'\n          ? ANALYTICS_EVENTS.AUTH_SIGNUP_STARTED\n          : ANALYTICS_EVENTS.AUTH_SIGNIN_STARTED\n\n      analytics.trackAuth(eventType, {\n        provider,\n        is_returning_user: false, // We don't know yet\n        auth_method: 'external_browser',\n      })\n\n      let authState = useAuthStore.getState().state\n\n      // Generate new auth state if not available\n      if (!authState) {\n        try {\n          // Generate fresh auth state from the main process\n          authState = await window.api.generateNewAuthState()\n\n          if (!authState) {\n            throw new Error('Generated auth state is null')\n          }\n\n          // Update the store with the new auth state\n          useAuthStore.getState().updateState(authState)\n        } catch (error) {\n          console.error('Failed to generate new auth state:', error)\n\n          // Track auth state generation failure\n          analytics.track(ANALYTICS_EVENTS.AUTH_STATE_GENERATION_FAILED, {\n            provider,\n            error_message:\n              error instanceof Error ? error.message : 'Unknown error',\n          })\n\n          throw new Error(\n            'Failed to generate auth state. Please restart the app.',\n          )\n        }\n      }\n\n      const params = new URLSearchParams({\n        response_type: 'code',\n        client_id: Auth0Config.clientId,\n        redirect_uri: Auth0Config.redirectUri,\n        scope: Auth0Config.scope,\n        prompt: 'select_account',\n        state: authState.state,\n        code_challenge: authState.codeChallenge,\n        code_challenge_method: 'S256',\n      })\n\n      // Add audience if configured\n      if (Auth0Config.audience) {\n        params.append('audience', Auth0Config.audience)\n      }\n\n      if (connection) {\n        params.append('connection', connection)\n      }\n\n      if (options?.email) {\n        params.append('login_hint', options.email)\n      }\n\n      if (options?.mode === 'signup') {\n        params.append('screen_hint', 'signup')\n      }\n\n      const authUrl = `https://${Auth0Config.domain}/authorize?${params.toString()}`\n\n      // Open in external browser\n      if (window.api?.invoke) {\n        await window.api.invoke('web-open-url', authUrl)\n      } else {\n        window.open(authUrl, '_blank')\n      }\n    },\n    [],\n  )\n\n  // Helper function to reduce duplication in social auth methods\n  const createSocialAuthMethod = useCallback(\n    (connection: string, providerName: string) => {\n      return async (email?: string) => {\n        try {\n          await openExternalAuth(connection, email ? { email } : undefined)\n        } catch (error) {\n          console.error(`${providerName} external auth failed:`, error)\n\n          // Track auth method failure\n          analytics.track(ANALYTICS_EVENTS.AUTH_METHOD_FAILED, {\n            provider: providerName.toLowerCase(),\n            error_message:\n              error instanceof Error ? error.message : 'Unknown error',\n            auth_method: 'external_browser',\n          })\n\n          throw error\n        }\n      }\n    },\n    [openExternalAuth],\n  )\n\n  // Social authentication methods - now use external browser by default\n  const loginWithGoogle = createSocialAuthMethod(\n    Auth0Connections.google,\n    'Google',\n  )\n  const loginWithMicrosoft = createSocialAuthMethod(\n    Auth0Connections.microsoft,\n    'Microsoft',\n  )\n  const loginWithApple = createSocialAuthMethod(Auth0Connections.apple, 'Apple')\n\n  // GitHub authentication - now uses external browser\n  const loginWithGitHub = createSocialAuthMethod(\n    Auth0Connections.github,\n    'GitHub',\n  )\n\n  // Email/password via Auth0 Database connection\n  const loginWithEmail = useCallback(\n    async (email?: string) => {\n      try {\n        await openExternalAuth(\n          Auth0Connections.database,\n          email ? { email, mode: 'login' } : { mode: 'login' },\n        )\n      } catch (error) {\n        console.error('Email login failed:', error)\n        analytics.track(ANALYTICS_EVENTS.AUTH_METHOD_FAILED, {\n          provider: 'email',\n          error_message:\n            error instanceof Error ? error.message : 'Unknown error',\n          auth_method: 'external_browser',\n        })\n        throw error\n      }\n    },\n    [openExternalAuth],\n  )\n\n  // Direct email/password login without opening a browser window\n  const loginWithEmailPassword = useCallback(\n    async (\n      email: string,\n      password: string,\n      options?: { skipNavigate?: boolean },\n    ) => {\n      try {\n        analytics.trackAuth(ANALYTICS_EVENTS.AUTH_SIGNIN_STARTED, {\n          provider: 'email',\n          is_returning_user: false,\n          auth_method: 'password_realm',\n        })\n\n        const result = await window.api.invoke('auth0-db-login', {\n          email,\n          password,\n        })\n        if (!result?.success || !result?.tokens) {\n          throw new Error(result?.error || 'Login failed')\n        }\n\n        const tokens = result.tokens as AuthTokens\n        const idToken = tokens.id_token || ''\n        let userInfo: AuthUser | null = null\n        try {\n          const payloadPart = idToken.split('.')[1]\n          const base64 = payloadPart.replace(/-/g, '+').replace(/_/g, '/')\n          const padded = base64 + '==='.slice((base64.length + 3) % 4)\n          const json = atob(padded)\n          const payload = JSON.parse(json)\n          userInfo = {\n            id: payload.sub || '',\n            email: payload.email,\n            name: payload.name,\n            picture: payload.picture,\n            provider:\n              typeof payload.sub === 'string' && payload.sub.includes('|')\n                ? payload.sub.split('|')[0]\n                : 'email',\n            lastSignInAt: new Date().toISOString(),\n          }\n        } catch {\n          console.warn(\n            'Failed to decode id_token, proceeding with minimal profile',\n          )\n          userInfo = {\n            id: 'email',\n            email,\n            provider: 'email',\n            lastSignInAt: new Date().toISOString(),\n          }\n        }\n\n        useAuthStore\n          .getState()\n          .setAuthData(tokens, userInfo as AuthUser, 'email')\n\n        analytics.trackAuth(ANALYTICS_EVENTS.AUTH_SIGNIN_COMPLETED, {\n          provider: 'email',\n          is_returning_user: false,\n          user_id: userInfo?.id,\n        })\n\n        if (!options?.skipNavigate) {\n          useMainStore.getState().setCurrentPage('home')\n        }\n\n        await window.api.notifyLoginSuccess(\n          userInfo,\n          tokens.id_token ?? null,\n          tokens.access_token ?? null,\n        )\n\n        // Hydrate per-user onboarding state\n        await hydrateOnboardingState('email/password')\n      } catch (error) {\n        console.error('Email/password login failed:', error)\n        analytics.track(ANALYTICS_EVENTS.AUTH_METHOD_FAILED, {\n          provider: 'email',\n          error_message:\n            error instanceof Error ? error.message : 'Unknown error',\n          auth_method: 'password_realm',\n        })\n        throw error\n      }\n    },\n    [],\n  )\n\n  const signupWithEmail = useCallback(\n    async (email?: string) => {\n      try {\n        await openExternalAuth(\n          Auth0Connections.database,\n          email ? { email, mode: 'signup' } : { mode: 'signup' },\n        )\n      } catch (error) {\n        console.error('Email signup failed:', error)\n        analytics.track(ANALYTICS_EVENTS.AUTH_METHOD_FAILED, {\n          provider: 'email',\n          error_message:\n            error instanceof Error ? error.message : 'Unknown error',\n          auth_method: 'external_browser',\n        })\n        throw error\n      }\n    },\n    [openExternalAuth],\n  )\n\n  // Directly create a database user via Auth0 Authentication API (proxied via main to avoid CORS)\n  const createDatabaseUser = useCallback(\n    async (email: string, password: string, name: string) => {\n      const result = await window.api.invoke('auth0-db-signup', {\n        email,\n        password,\n        name,\n      })\n      if (!result?.success) {\n        throw new Error(result?.error || 'Signup failed')\n      }\n      return result.data\n    },\n    [],\n  )\n\n  // Self-hosted authentication - bypasses all external auth\n  const loginWithSelfHosted = useCallback(async () => {\n    try {\n      // Track self-hosted signin attempt\n      analytics.trackAuth(ANALYTICS_EVENTS.AUTH_SIGNIN_STARTED, {\n        provider: 'self-hosted',\n        is_returning_user: false,\n        auth_method: 'self_hosted',\n      })\n\n      setSelfHostedMode()\n\n      // Notify main process about self-hosted login and wait for it to complete\n      const selfHostedProfile = {\n        id: 'self-hosted',\n        provider: 'self-hosted',\n        lastSignInAt: new Date().toISOString(),\n      }\n\n      await window.api.notifyLoginSuccess(\n        selfHostedProfile,\n        null, // No idToken for self-hosted\n        null, // No accessToken for self-hosted\n      )\n\n      // Hydrate per-user onboarding state\n      await hydrateOnboardingState('self-hosted')\n\n      // Track successful self-hosted signin\n      analytics.trackAuth(ANALYTICS_EVENTS.AUTH_SIGNIN_COMPLETED, {\n        provider: 'self-hosted',\n        is_returning_user: false,\n        user_id: 'self-hosted',\n        auth_method: 'self_hosted',\n      })\n    } catch (error) {\n      console.error('Self-hosted mode activation error:', error)\n\n      // Track self-hosted signin failure\n      analytics.track(ANALYTICS_EVENTS.AUTH_SIGNIN_FAILED, {\n        provider: 'self-hosted',\n        error_message: error instanceof Error ? error.message : 'Unknown error',\n        auth_method: 'self_hosted',\n      })\n\n      throw error\n    }\n  }, [setSelfHostedMode])\n\n  // Get access token for API calls\n  const getAccessToken = useCallback(async () => {\n    try {\n      // First try to get from our store (for external auth)\n      if (tokens?.access_token) {\n        return tokens.access_token\n      }\n\n      // Fallback to Auth0 silent auth (for popup/redirect auth)\n      return await getAccessTokenSilently()\n    } catch (error) {\n      console.error('Error getting access token:', error)\n      throw error\n    }\n  }, [getAccessTokenSilently, tokens])\n\n  // Manual token refresh\n  const refreshTokens = useCallback(async () => {\n    try {\n      console.log('Manually refreshing tokens...')\n      const result = await window.api.invoke('refresh-tokens')\n\n      if (result.success) {\n        console.log('Manual token refresh successful')\n        return result\n      } else {\n        console.error('Manual token refresh failed:', result.error)\n        throw new Error(result.error)\n      }\n    } catch (error) {\n      console.error('Error during manual token refresh:', error)\n      throw error\n    }\n  }, [])\n\n  // Logout\n  const logoutUser = useCallback(\n    async (completelySignOut: boolean = false) => {\n      try {\n        // Track logout attempt\n        const currentUser = authUser\n        analytics.trackAuth(ANALYTICS_EVENTS.AUTH_LOGOUT, {\n          provider: currentUser?.provider || 'unknown',\n          is_returning_user: true, // If they're logging out, they were logged in\n          user_id: currentUser?.id,\n          complete_signout: completelySignOut,\n          session_duration_ms: analytics.getSessionDuration(),\n        })\n\n        // Clear main process store first\n        await window.api.logout()\n\n        // Clear our auth store, preserving user data by default\n        clearAuth(!completelySignOut)\n\n        // Also logout from Auth0 if using Auth0 session\n        if (auth0IsAuthenticated) {\n          logout({\n            logoutParams: {\n              returnTo: window.location.origin,\n            },\n          })\n        }\n      } catch (error) {\n        console.error('Error during logout:', error)\n\n        // Track logout failure\n        analytics.track(ANALYTICS_EVENTS.AUTH_LOGOUT_FAILED, {\n          provider: authUser?.provider || 'unknown',\n          error_message:\n            error instanceof Error ? error.message : 'Unknown error',\n          complete_signout: completelySignOut,\n        })\n\n        // Still try to clear local auth even if main process logout fails\n        clearAuth(!completelySignOut)\n      }\n    },\n    [logout, clearAuth, auth0IsAuthenticated, authUser],\n  )\n\n  // Handle auth token events from main process\n  useEffect(() => {\n    if (!window.api?.on) {\n      console.warn('window.api.on not available')\n      return\n    }\n\n    // Check if listener is already set up\n    if ((window as any).__authTokenListenerSetup) {\n      return\n    }\n\n    // Mark that we've set up the listener\n    ;(window as any).__authTokenListenerSetup = true\n\n    // Handle token refresh success\n    const cleanupTokensRefreshed = window.api.on(\n      'tokens-refreshed',\n      async (newTokens: AuthTokens) => {\n        console.log('Tokens refreshed successfully, updating auth store')\n\n        try {\n          // Update the auth store with refreshed tokens\n          const currentUser = authUser\n          if (currentUser) {\n            useAuthStore\n              .getState()\n              .setAuthData(newTokens, currentUser, currentUser.provider)\n\n            // Track successful token refresh\n            analytics.track(ANALYTICS_EVENTS.AUTH_SIGNIN_COMPLETED, {\n              provider: currentUser.provider || 'unknown',\n              user_id: currentUser.id,\n              is_returning_user: true,\n              reason: 'token_refresh',\n            })\n          }\n        } catch (error) {\n          console.error(\n            'Error updating auth store with refreshed tokens:',\n            error,\n          )\n        }\n      },\n    )\n\n    // Handle token expiration (when refresh fails or no refresh token available)\n    const cleanupTokenExpired = window.api.on(\n      'auth-token-expired',\n      async () => {\n        console.log('Auth token expired, automatically signing out user')\n\n        try {\n          // Track automatic logout due to token expiration\n          const currentUser = authUser\n          analytics.trackAuth(ANALYTICS_EVENTS.AUTH_LOGOUT, {\n            provider: currentUser?.provider || 'unknown',\n            is_returning_user: true,\n            user_id: currentUser?.id,\n            complete_signout: false,\n            session_duration_ms: analytics.getSessionDuration(),\n            reason: 'token_expired',\n          })\n\n          logoutUser(false)\n\n          // Auth state will automatically redirect to welcome page\n        } catch (error) {\n          console.error('Error during automatic logout:', error)\n\n          // Still try to clear local auth even if main process logout fails\n          clearAuth(true)\n        }\n      },\n    )\n\n    return () => {\n      cleanupTokensRefreshed()\n      cleanupTokenExpired()\n      ;(window as any).__authTokenListenerSetup = false\n    }\n  }, [logout, clearAuth, auth0IsAuthenticated, authUser, logoutUser])\n\n  return {\n    // Auth state\n    user: authUser,\n    isAuthenticated,\n    isLoading,\n    error,\n\n    // Authentication methods\n    loginWithGoogle,\n    loginWithMicrosoft,\n    loginWithApple,\n    loginWithGitHub,\n    loginWithEmail,\n    loginWithEmailPassword,\n    signupWithEmail,\n    createDatabaseUser,\n    loginWithSelfHosted,\n    logoutUser,\n\n    // Utilities\n    getAccessToken,\n    getIdTokenClaims,\n    refreshTokens,\n  }\n}\n"
  },
  {
    "path": "app/components/home/HomeKit.tsx",
    "content": "import {\n  Home,\n  BookOpen,\n  FileText,\n  CogFour,\n  InfoCircle,\n} from '@mynaui/icons-react'\nimport { ItoIcon } from '../icons/ItoIcon'\nimport { useMainStore } from '@/app/store/useMainStore'\nimport { useUserMetadataStore } from '@/app/store/useUserMetadataStore'\nimport { useOnboardingStore } from '@/app/store/useOnboardingStore'\nimport { useAuth } from '@/app/components/auth/useAuth'\nimport useBillingState from '@/app/hooks/useBillingState'\nimport { PaidStatus } from '@/lib/main/sqlite/models'\nimport { useEffect, useState, useRef } from 'react'\nimport { NavItem } from '../ui/nav-item'\nimport HomeContent from './contents/HomeContent'\nimport DictionaryContent from './contents/DictionaryContent'\nimport NotesContent from './contents/NotesContent'\nimport SettingsContent from './contents/SettingsContent'\nimport AboutContent from './contents/AboutContent'\n\nexport default function HomeKit() {\n  const { navExpanded, currentPage, setCurrentPage } = useMainStore()\n  const { metadata } = useUserMetadataStore()\n  const { onboardingCompleted } = useOnboardingStore()\n  const { isAuthenticated, user } = useAuth()\n  const billingState = useBillingState()\n  const [showText, setShowText] = useState(navExpanded)\n  const hasStartedTrialRef = useRef(false)\n  const previousUserIdRef = useRef<string | undefined>(undefined)\n  const [isStartingTrial, setIsStartingTrial] = useState(false)\n\n  const isPro =\n    metadata?.paid_status === PaidStatus.PRO ||\n    metadata?.paid_status === PaidStatus.PRO_TRIAL ||\n    billingState.proStatus === 'active_pro' ||\n    billingState.proStatus === 'free_trial'\n\n  // Reset flags when user changes\n  useEffect(() => {\n    const currentUserId = user?.id\n    const previousUserId = previousUserIdRef.current\n\n    if (currentUserId && currentUserId !== previousUserId) {\n      // User changed - reset trial start flag\n      hasStartedTrialRef.current = false\n      setIsStartingTrial(false)\n      previousUserIdRef.current = currentUserId\n    } else if (currentUserId && previousUserId === undefined) {\n      // First time setting userId\n      previousUserIdRef.current = currentUserId\n    }\n  }, [user?.id])\n\n  // Start trial for users who don't have one yet\n  // Case 1: New users after onboarding completes\n  // Case 2: Existing users who completed onboarding but haven't started trial yet\n  useEffect(() => {\n    // Skip if still loading billing state or not authenticated\n    if (billingState.isLoading || !isAuthenticated) return\n\n    // Only proceed if onboarding is completed\n    if (!onboardingCompleted) return\n\n    // Check if user has a trial or subscription\n    const hasTrialOrSubscription =\n      billingState.proStatus === 'free_trial' ||\n      billingState.proStatus === 'active_pro' ||\n      isPro\n\n    // Start trial if:\n    // 1. User hasn't started trial yet (tracked by ref)\n    // 2. User doesn't have a trial or subscription\n    // 3. User has completed onboarding\n    if (!hasStartedTrialRef.current && !hasTrialOrSubscription) {\n      hasStartedTrialRef.current = true\n      setIsStartingTrial(true) // Set flag to indicate trial is being started\n      // Start trial\n      window.api.trial.startAfterOnboarding().catch(err => {\n        console.error('Failed to start trial:', err)\n        // Reset flag so we can retry if needed\n        hasStartedTrialRef.current = false\n        setIsStartingTrial(false)\n      })\n    }\n  }, [\n    onboardingCompleted,\n    isAuthenticated,\n    billingState.isLoading,\n    billingState.proStatus,\n    isPro,\n  ])\n\n  // Listen for trial-started event to refresh billing state\n  useEffect(() => {\n    const offTrialStarted = window.api.on('trial-started', async () => {\n      // Trial started successfully - refresh billing state\n      // HomeContent will handle showing the dialog based on billing state transition\n      await billingState.refresh()\n      setIsStartingTrial(false) // Reset flag after trial starts\n    })\n\n    return () => {\n      offTrialStarted?.()\n    }\n  }, [billingState])\n\n  // Reset trial start flag when onboarding resets\n  useEffect(() => {\n    if (!onboardingCompleted) {\n      hasStartedTrialRef.current = false\n      setIsStartingTrial(false)\n    }\n  }, [onboardingCompleted])\n\n  // Listen for billing deep-link events and finalize subscription\n  useEffect(() => {\n    const offSuccess = window.api.on(\n      'billing-session-completed',\n      async (sessionId: string) => {\n        try {\n          if (sessionId) {\n            await window.api.billing.confirmSession(sessionId)\n          }\n          // Ensure trial is completed locally and on server\n          await window.api.trial.complete()\n        } catch (err) {\n          console.error('Failed to finalize billing session', err)\n        }\n      },\n    )\n\n    const offCancel = window.api.on('billing-session-cancelled', () => {\n      // No-op for now; could show a toast in the future\n    })\n\n    return () => {\n      offSuccess?.()\n      offCancel?.()\n    }\n  }, [])\n\n  // Handle text and positioning animation timing\n  useEffect(() => {\n    if (navExpanded) {\n      // When expanding: slide right first, then show text\n      const timer = setTimeout(() => {\n        setShowText(true) // Show text after slide starts\n      }, 75)\n      return () => clearTimeout(timer)\n    } else {\n      // When collapsing: hide text immediately, then center icons after slide completes\n      setShowText(false)\n      // Return no-op function\n      return () => {}\n    }\n  }, [navExpanded])\n\n  // Render the appropriate content based on current page\n  const renderContent = () => {\n    switch (currentPage) {\n      case 'home':\n        return <HomeContent isStartingTrial={isStartingTrial} />\n      case 'dictionary':\n        return <DictionaryContent />\n      case 'notes':\n        return <NotesContent />\n      case 'settings':\n        return <SettingsContent />\n      case 'about':\n        return <AboutContent />\n      default:\n        return <HomeContent />\n    }\n  }\n\n  return (\n    <div className=\"flex h-full\">\n      {/* Sidebar */}\n      <div\n        className={`${navExpanded ? 'w-48' : 'w-20'} flex flex-col justify-between py-4 px-4 transition-all duration-100 ease-in-out border-r border-neutral-200`}\n      >\n        <div>\n          {/* Logo and Plan */}\n          <div className=\"flex items-center mb-10 px-3\">\n            <ItoIcon\n              className=\"w-6 text-gray-900 flex-shrink-0\"\n              style={{ height: '32px' }}\n            />\n            <span\n              className={`text-2xl font-bold transition-opacity duration-100 ${showText ? 'opacity-100' : 'opacity-0'} ${showText ? 'ml-2' : 'w-0 overflow-hidden'}`}\n            >\n              ito\n            </span>\n            {isPro && showText && (\n              <span\n                className={`text-xs font-semibold px-2 py-0.5 rounded-md bg-gradient-to-r from-purple-500 to-pink-500 text-white transition-opacity duration-100 ${showText ? 'opacity-100' : 'opacity-0'} ${showText ? 'ml-2' : 'w-0 overflow-hidden'}`}\n              >\n                PRO\n              </span>\n            )}\n          </div>\n          {/* Nav */}\n          <div className=\"flex flex-col gap-1 text-sm\">\n            <NavItem\n              icon={<Home className=\"w-5 h-5\" />}\n              label=\"Home\"\n              isActive={currentPage === 'home'}\n              showText={showText}\n              onClick={() => setCurrentPage('home')}\n            />\n            <NavItem\n              icon={<BookOpen className=\"w-5 h-5\" />}\n              label=\"Dictionary\"\n              isActive={currentPage === 'dictionary'}\n              showText={showText}\n              onClick={() => setCurrentPage('dictionary')}\n            />\n            <NavItem\n              icon={<FileText className=\"w-5 h-5\" />}\n              label=\"Notes\"\n              isActive={currentPage === 'notes'}\n              showText={showText}\n              onClick={() => setCurrentPage('notes')}\n            />\n            <NavItem\n              icon={<CogFour className=\"w-5 h-5\" />}\n              label=\"Settings\"\n              isActive={currentPage === 'settings'}\n              showText={showText}\n              onClick={() => setCurrentPage('settings')}\n            />\n            <NavItem\n              icon={<InfoCircle className=\"w-5 h-5\" />}\n              label=\"About\"\n              isActive={currentPage === 'about'}\n              showText={showText}\n              onClick={() => setCurrentPage('about')}\n            />\n          </div>\n        </div>\n      </div>\n\n      {/* Main Content */}\n      <div className=\"flex flex-col flex-1 items-center bg-white rounded-lg m-2 ml-0 mt-0 pt-12\">\n        {renderContent()}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/home/ProUpgradeDialog.tsx",
    "content": "import React, { useState, useEffect } from 'react'\nimport { Check } from '@mynaui/icons-react'\nimport { Dialog, DialogContent, DialogFooter } from '@/app/components/ui/dialog'\nimport { Button } from '@/app/components/ui/button'\nimport proBannerImage from '@/app/assets/pro-banner.png'\nimport useBillingState from '@/app/hooks/useBillingState'\n\ninterface ProUpgradeDialogProps {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}\n\nexport function ProUpgradeDialog({\n  open,\n  onOpenChange,\n}: ProUpgradeDialogProps) {\n  const billingState = useBillingState()\n  const [checkoutLoading, setCheckoutLoading] = useState(false)\n  const [checkoutError, setCheckoutError] = useState<string | null>(null)\n\n  // Refresh billing state when checkout session completes\n  useEffect(() => {\n    const offSuccess = window.api.on('billing-session-completed', async () => {\n      // Refresh billing state to reflect the new subscription\n      await billingState.refresh()\n      setCheckoutError(null)\n      // Close the dialog after successful checkout\n      onOpenChange(false)\n    })\n\n    return () => {\n      offSuccess?.()\n    }\n  }, [billingState, onOpenChange])\n\n  const handleCheckout = async () => {\n    setCheckoutLoading(true)\n    setCheckoutError(null)\n    try {\n      const res = await window.api.billing.createCheckoutSession()\n      if (res?.success && res?.url) {\n        await window.api.invoke('web-open-url', res.url)\n      } else {\n        setCheckoutError(\n          res?.error || 'Failed to create checkout session. Please try again.',\n        )\n      }\n    } catch (err: any) {\n      setCheckoutError(\n        err?.message || 'Failed to create checkout session. Please try again.',\n      )\n    } finally {\n      setCheckoutLoading(false)\n    }\n  }\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-md p-0 overflow-hidden border-none\">\n        {/* Banner Header with Image */}\n        <div\n          className=\"relative px-8 py-12 text-center bg-cover bg-center\"\n          style={{ backgroundImage: `url(${proBannerImage})` }}\n        >\n          {/* PRO Badge */}\n          <div className=\"relative inline-block mb-6\">\n            <div className=\"bg-white rounded-full px-12 py-4 shadow-lg\">\n              <span className=\"text-5xl font-black bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent\">\n                PRO\n              </span>\n            </div>\n          </div>\n        </div>\n\n        {/* Content */}\n        <div className=\"px-8 py-6 bg-white\">\n          <h2 className=\"text-3xl font mb-2 \">\n            Congrats! You have been{' '}\n            <span className=\"bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent\">\n              upgraded to Ito Pro for free!\n            </span>\n          </h2>\n\n          <p className=\"text-l text-gray-600  mb-6\">\n            Enjoy all Pro features for{' '}\n            <span className=\"font-semibold\">14 days</span>.\n          </p>\n\n          {/* Error Message */}\n          {checkoutError && (\n            <div className=\"bg-red-50 border border-red-200 rounded-lg p-4 text-sm text-red-800 mb-6\">\n              {checkoutError}\n            </div>\n          )}\n\n          {/* Features List */}\n          <div className=\"space-y-3 mb-6 border border-gray-200 rounded-lg p-4\">\n            <FeatureItem text=\"Unlimited words per week\" />\n            <FeatureItem text=\"Ultra fast dictation as fast as 0.3 second\" />\n            <FeatureItem text=\"Priority customer support\" />\n            <FeatureItem text=\"Early access to new functionality\" />\n          </div>\n\n          {/* Buttons */}\n          <DialogFooter className=\"flex-row justify-between sm:justify-between\">\n            <Button\n              onClick={() => onOpenChange(false)}\n              variant=\"default\"\n              size=\"lg\"\n              className=\"bg-gray-900 hover:bg-gray-800 text-white rounded-xl\"\n            >\n              Try for free\n            </Button>\n            <Button\n              onClick={handleCheckout}\n              variant=\"outline\"\n              size=\"lg\"\n              className=\"rounded-xl border-gray-200\"\n              disabled={checkoutLoading || billingState.isLoading}\n            >\n              {checkoutLoading ? 'Loading...' : 'Upgrade Now'}{' '}\n              <span className=\"text-gray-500\">(20% off)</span>\n            </Button>\n          </DialogFooter>\n        </div>\n      </DialogContent>\n    </Dialog>\n  )\n}\n\nfunction FeatureItem({ text }: { text: string }) {\n  return (\n    <div className=\"flex items-center gap-3\">\n      <div className=\"flex-shrink-0\">\n        <Check className=\"w-5 h-5\" strokeWidth={3} />\n      </div>\n      <span className=\"text-gray-900\">{text}</span>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/home/contents/AboutContent.tsx",
    "content": "import { Button } from '@/app/components/ui/button'\nimport DiscordIcon from '@/app/components/icons/DiscordIcon'\nimport XIcon from '@/app/components/icons/XIcon'\nimport GitHubIcon from '@/app/components/icons/GitHubIcon'\nimport { Globe, Telephone } from '@mynaui/icons-react'\nimport { EXTERNAL_LINKS } from '@/lib/constants/external-links'\nimport ItoIcon from '../../icons/ItoIcon'\n\ninterface AboutCardProps {\n  icon: React.ReactNode\n  title: string\n  description: string\n  buttonText: string\n  onClick: () => void\n}\n\nfunction AboutCard({\n  icon,\n  title,\n  description,\n  buttonText,\n  onClick,\n}: AboutCardProps) {\n  return (\n    <div className=\"w-1/3 bg-white rounded-lg border border-gray-200 p-4 flex flex-col items-start text-left\">\n      <div className=\"w-8 h-8 bg-white rounded-lg flex items-center justify-center mb-3\">\n        {icon}\n      </div>\n      <h2 className=\"text-lg font-semibold mb-1\">{title}</h2>\n      <p className=\"text-gray-500 mb-6 leading-relaxed\">{description}</p>\n      <Button\n        onClick={onClick}\n        className=\"w-fit bg-white text-black border border-gray-300 hover:bg-gray-50 rounded-full cursor-pointer\"\n        style={{\n          padding: '20px 28px',\n        }}\n      >\n        {buttonText}\n      </Button>\n    </div>\n  )\n}\n\nexport default function AboutContent() {\n  const handleDiscordClick = () => {\n    window.open(EXTERNAL_LINKS.DISCORD, '_blank')\n  }\n\n  const handleTeamCallClick = () => {\n    window.open(EXTERNAL_LINKS.TEAM_CALL, '_blank')\n  }\n\n  const handleXClick = () => {\n    window.open(EXTERNAL_LINKS.X_TWITTER, '_blank')\n  }\n\n  const handleGitHubClick = () => {\n    window.open(EXTERNAL_LINKS.GITHUB, '_blank')\n  }\n\n  const handleWebsiteClick = () => {\n    window.open(EXTERNAL_LINKS.WEBSITE, '_blank')\n  }\n\n  return (\n    <div className=\"w-full px-24\">\n      <div className=\"mb-8\">\n        <h1 className=\"text-2xl font-medium\">About</h1>\n      </div>\n\n      <div className=\"flex flex-col gap-4\">\n        {/* First Row: 3 items */}\n        <div className=\"flex flex-row gap-4\">\n          <AboutCard\n            icon={<DiscordIcon width={24} height={24} className=\"text-black\" />}\n            title=\"Discord\"\n            description=\"Join the community, share feedback, and grow with Ito.\"\n            buttonText=\"Join Discord\"\n            onClick={handleDiscordClick}\n          />\n\n          <AboutCard\n            icon={<Telephone className=\"w-6 h-6 text-black\" />}\n            title=\"Team Call\"\n            description=\"Got feedback or ideas? Book a quick call with the Ito team.\"\n            buttonText=\"Book a Call\"\n            onClick={handleTeamCallClick}\n          />\n\n          <AboutCard\n            icon={<XIcon width={24} height={24} className=\"text-black\" />}\n            title=\"X (Twitter)\"\n            description=\"Get updates, tips, and behind-the-scenes insights from the Ito team.\"\n            buttonText=\"Follow on X\"\n            onClick={handleXClick}\n          />\n        </div>\n\n        {/* Second Row: 2 items */}\n        <div className=\"flex flex-row gap-4\">\n          <AboutCard\n            icon={<GitHubIcon width={24} height={24} className=\"text-black\" />}\n            title=\"GitHub\"\n            description=\"Check out the code, contribute, or star the repo.\"\n            buttonText=\"View on GitHub\"\n            onClick={handleGitHubClick}\n          />\n\n          <AboutCard\n            icon={<Globe className=\"w-6 h-6 text-black\" />}\n            title=\"ito.ai\"\n            description=\"Learn more about Ito, explore features, and see what's next.\"\n            buttonText=\"Go to Website\"\n            onClick={handleWebsiteClick}\n          />\n\n          <div className=\"w-1/3 bg-white rounded-lg border border-gray-200 p-4 flex flex-col items-start text-left\">\n            <div className=\"bg-white rounded-lg flex items-center justify-center mb-4\">\n              <ItoIcon\n                className=\"w-6 h-6 text-gray-900\"\n                style={{ height: '24px' }}\n              />\n              <span className={`text-lg font-bold ml-2`}>ito</span>\n            </div>\n            <h2 className=\"text-lg font-semibold mb-4\">\n              Version {import.meta.env.VITE_ITO_VERSION}\n            </h2>\n            <p className=\"text-gray-500 mb-6 leading-relaxed\">\n              Made with 🩷 in San Francisco.\n            </p>\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/home/contents/DictionaryContent.tsx",
    "content": "import { useEffect, useRef, useState } from 'react'\nimport { ArrowUp, Pencil, Trash, Plus } from '@mynaui/icons-react'\nimport { Tooltip, TooltipTrigger, TooltipContent } from '../../ui/tooltip'\nimport { Switch } from '../../ui/switch'\nimport { StatusIndicator } from '../../ui/status-indicator'\nimport { useDictionaryStore } from '../../../store/useDictionaryStore'\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogFooter,\n} from '../../ui/dialog'\nimport { Button } from '../../ui/button'\n\nexport default function DictionaryContent() {\n  const {\n    entries,\n    loadEntries,\n    addEntry,\n    addReplacement,\n    updateEntry,\n    deleteEntry,\n  } = useDictionaryStore()\n  const [showScrollToTop, setShowScrollToTop] = useState(false)\n  const [hoveredRow, setHoveredRow] = useState<number | null>(null)\n  const [editingEntry, setEditingEntry] = useState<{\n    id: string\n    type: 'normal' | 'replacement'\n    content?: string\n    from?: string\n    to?: string\n  } | null>(null)\n  const [editContent, setEditContent] = useState('')\n  const [editFrom, setEditFrom] = useState('')\n  const [editTo, setEditTo] = useState('')\n  const [showAddDialog, setShowAddDialog] = useState(false)\n  const [newEntryContent, setNewEntryContent] = useState('')\n  const [newFrom, setNewFrom] = useState('')\n  const [newTo, setNewTo] = useState('')\n  const [isReplacement, setIsReplacement] = useState(false)\n  const [statusIndicator, setStatusIndicator] = useState<\n    'success' | 'error' | null\n  >(null)\n  const [errorMessage, setErrorMessage] = useState<string>('')\n  const [successMessage, setSuccessMessage] = useState<string>('')\n  const containerRef = useRef<HTMLDivElement>(null)\n  const editInputRef = useRef<HTMLInputElement>(null)\n  const editFromRef = useRef<HTMLInputElement>(null)\n  const addInputRef = useRef<HTMLInputElement>(null)\n  const addFromRef = useRef<HTMLInputElement>(null)\n\n  useEffect(() => {\n    loadEntries()\n  }, [loadEntries])\n\n  // Handle scroll events\n  useEffect(() => {\n    const handleScroll = () => {\n      if (containerRef.current) {\n        const scrollTop = containerRef.current.scrollTop\n        setShowScrollToTop(scrollTop > 200) // Show button after scrolling 200px\n      }\n    }\n\n    const container = containerRef.current\n    if (container) {\n      container.addEventListener('scroll', handleScroll)\n      return () => container.removeEventListener('scroll', handleScroll)\n    }\n\n    return undefined\n  }, [])\n\n  const scrollToTop = () => {\n    if (containerRef.current) {\n      containerRef.current.scrollTo({\n        top: 0,\n        behavior: 'smooth',\n      })\n    }\n  }\n\n  const getDisplayText = (entry: (typeof entries)[0]) => {\n    if (entry.type === 'replacement') {\n      return `${entry.from} → ${entry.to}`\n    }\n    return entry.content\n  }\n\n  const handleEdit = (id: string) => {\n    const entry = entries.find(e => e.id === id)\n    if (entry) {\n      if (entry.type === 'normal') {\n        setEditingEntry({ id, type: 'normal', content: entry.content })\n        setEditContent(entry.content)\n        setEditFrom('')\n        setEditTo('')\n        // Focus the input after the dialog opens\n        setTimeout(() => {\n          editInputRef.current?.focus()\n        }, 100)\n      } else {\n        setEditingEntry({\n          id,\n          type: 'replacement',\n          from: entry.from,\n          to: entry.to,\n        })\n        setEditContent('')\n        setEditFrom(entry.from)\n        setEditTo(entry.to)\n        // Focus the first input after the dialog opens\n        setTimeout(() => {\n          editFromRef.current?.focus()\n        }, 100)\n      }\n    }\n  }\n\n  const handleSaveEdit = async () => {\n    if (!editingEntry) return\n\n    try {\n      if (editingEntry.type === 'normal' && editContent.trim() !== '') {\n        await updateEntry(editingEntry.id, {\n          type: 'normal',\n          content: editContent.trim(),\n        } as any)\n        setEditingEntry(null)\n        setEditContent('')\n        setErrorMessage('')\n        setSuccessMessage(`\"${editContent.trim()}\" updated successfully`)\n        setStatusIndicator('success')\n      } else if (\n        editingEntry.type === 'replacement' &&\n        editFrom.trim() !== '' &&\n        editTo.trim() !== ''\n      ) {\n        await updateEntry(editingEntry.id, {\n          type: 'replacement',\n          from: editFrom.trim(),\n          to: editTo.trim(),\n        } as any)\n        setEditingEntry(null)\n        setEditFrom('')\n        setEditTo('')\n        setErrorMessage('')\n        setSuccessMessage(\n          `\"${editFrom.trim()}\" → \"${editTo.trim()}\" updated successfully`,\n        )\n        setStatusIndicator('success')\n      }\n    } catch (error: any) {\n      console.error('Failed to update dictionary entry:', error)\n      const errorMsg = error?.message || 'Failed to update dictionary entry'\n      setErrorMessage(errorMsg)\n      setStatusIndicator('error')\n    }\n  }\n\n  const handleCancelEdit = () => {\n    setEditingEntry(null)\n    setEditContent('')\n    setEditFrom('')\n    setEditTo('')\n  }\n\n  const handleDelete = async (id: string) => {\n    const entryToDelete = entries.find(e => e.id === id)\n    if (entryToDelete) {\n      const deletedItemText = getDisplayText(entryToDelete)\n      try {\n        await deleteEntry(id)\n        setErrorMessage('')\n        setSuccessMessage(`\"${deletedItemText}\" deleted successfully`)\n        setStatusIndicator('success')\n      } catch (error) {\n        console.error('Failed to delete dictionary entry:', error)\n        setErrorMessage(`Failed to delete \"${deletedItemText}\"`)\n        setStatusIndicator('error')\n      }\n    }\n  }\n\n  const handleAddNew = () => {\n    setShowAddDialog(true)\n    setNewEntryContent('')\n    setNewFrom('')\n    setNewTo('')\n    setIsReplacement(false)\n    // Focus the input after the dialog opens\n    setTimeout(() => {\n      addInputRef.current?.focus()\n    }, 100)\n  }\n\n  const handleSaveNew = async () => {\n    try {\n      if (isReplacement) {\n        if (newFrom.trim() !== '' && newTo.trim() !== '') {\n          await addReplacement(newFrom.trim(), newTo.trim())\n          setShowAddDialog(false)\n          setNewFrom('')\n          setNewTo('')\n          setErrorMessage('')\n          setSuccessMessage(\n            `\"${newFrom.trim()}\" → \"${newTo.trim()}\" added successfully`,\n          )\n          setStatusIndicator('success')\n        }\n      } else {\n        if (newEntryContent.trim() !== '') {\n          await addEntry(newEntryContent.trim())\n          setShowAddDialog(false)\n          setNewEntryContent('')\n          setErrorMessage('')\n          setSuccessMessage(`\"${newEntryContent.trim()}\" added successfully`)\n          setStatusIndicator('success')\n        }\n      }\n    } catch (error: any) {\n      console.error('Failed to add dictionary entry:', error)\n      const errorMsg = error?.message || 'Failed to add dictionary entry'\n      setErrorMessage(errorMsg)\n      setStatusIndicator('error')\n    }\n  }\n\n  const handleCancelNew = () => {\n    setShowAddDialog(false)\n    setNewEntryContent('')\n    setNewFrom('')\n    setNewTo('')\n    setIsReplacement(false)\n  }\n\n  const handleReplacementToggle = (checked: boolean) => {\n    setIsReplacement(checked)\n    // Focus appropriate input when toggling\n    setTimeout(() => {\n      if (checked) {\n        addFromRef.current?.focus()\n      } else {\n        addInputRef.current?.focus()\n      }\n    }, 100)\n  }\n\n  // Handle keyboard shortcuts in dialogs\n  const handleEditKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === 'Enter') {\n      e.preventDefault()\n      handleSaveEdit()\n    } else if (e.key === 'Escape') {\n      e.preventDefault()\n      handleCancelEdit()\n    }\n  }\n\n  const handleAddKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === 'Enter') {\n      e.preventDefault()\n      handleSaveNew()\n    } else if (e.key === 'Escape') {\n      e.preventDefault()\n      handleCancelNew()\n    }\n  }\n\n  const noEntries = entries.length === 0\n\n  return (\n    <div\n      ref={containerRef}\n      className=\"w-full px-24 max-h-160 overflow-y-auto relative\"\n      style={{\n        msOverflowStyle: 'none',\n        scrollbarWidth: 'none',\n      }}\n    >\n      <div className=\"flex items-center justify-between mb-8\">\n        <h1 className=\"text-2xl font-medium\">Dictionary</h1>\n        <button\n          onClick={handleAddNew}\n          className=\"bg-gray-900 text-white px-6 py-3 rounded-full font-semibold hover:bg-gray-800 cursor-pointer flex items-center gap-2\"\n        >\n          <Plus className=\"w-4 h-4\" />\n          Add new\n        </button>\n      </div>\n\n      <div className=\"w-full h-[1px] bg-slate-200 my-10\"></div>\n      {noEntries && (\n        <div className=\"text-gray-500\">\n          <p className=\"text-sm\">No entries yet</p>\n          <p className=\"text-xs mt-1\">\n            Dictionary entries make the transcription more accurate\n          </p>\n        </div>\n      )}\n      {!noEntries && (\n        <div className=\"bg-white rounded-lg border border-slate-200 divide-y divide-slate-200\">\n          {entries.map((entry, index) => (\n            <div\n              key={entry.id}\n              className=\"flex items-center justify-between px-4 py-4 gap-10 hover:bg-gray-50 transition-colors duration-200 group\"\n              onMouseEnter={() => setHoveredRow(index)}\n              onMouseLeave={() => setHoveredRow(null)}\n            >\n              <div className=\"text-gray-900 flex-1\">\n                {getDisplayText(entry)}\n              </div>\n\n              {/* Action Icons - shown on hover */}\n              <div\n                className={`flex items-center gap-2 transition-opacity duration-200 ${\n                  hoveredRow === index ? 'opacity-100' : 'opacity-0'\n                }`}\n              >\n                <Tooltip>\n                  <TooltipTrigger asChild>\n                    <button\n                      onClick={() => handleEdit(entry.id)}\n                      className=\"p-1.5 hover:bg-gray-200 rounded transition-colors cursor-pointer\"\n                      aria-label=\"Edit entry\"\n                    >\n                      <Pencil className=\"w-4 h-4 text-gray-600\" />\n                    </button>\n                  </TooltipTrigger>\n                  <TooltipContent side=\"top\" sideOffset={5}>\n                    Edit\n                  </TooltipContent>\n                </Tooltip>\n\n                <Tooltip>\n                  <TooltipTrigger asChild>\n                    <button\n                      onClick={() => handleDelete(entry.id)}\n                      className=\"p-1.5 hover:bg-red-100 rounded transition-colors cursor-pointer\"\n                      aria-label=\"Delete entry\"\n                    >\n                      <Trash className=\"w-4 h-4 text-gray-600 hover:text-red-600\" />\n                    </button>\n                  </TooltipTrigger>\n                  <TooltipContent side=\"top\" sideOffset={5}>\n                    Delete\n                  </TooltipContent>\n                </Tooltip>\n              </div>\n            </div>\n          ))}\n        </div>\n      )}\n\n      {/* Scroll to Top Button */}\n      {showScrollToTop && (\n        <button\n          onClick={scrollToTop}\n          className=\"fixed bottom-8 bg-black text-white right-8 w-8 h-8 rounded-full shadow-lg hover:shadow-xl hover:-translate-y-1 transition-all duration-200 flex items-center justify-center group z-50 cursor-pointer\"\n          aria-label=\"Scroll to top\"\n        >\n          <ArrowUp className=\"w-4 h-4 font-bold\" />\n        </button>\n      )}\n\n      {/* Status Indicator */}\n      <StatusIndicator\n        status={statusIndicator}\n        onHide={() => {\n          setStatusIndicator(null)\n          setErrorMessage('')\n          setSuccessMessage('')\n        }}\n        successMessage={successMessage || 'Dictionary entry added successfully'}\n        errorMessage={errorMessage || 'Failed to add dictionary entry'}\n      />\n\n      {/* Edit Entry Dialog */}\n      <Dialog\n        open={!!editingEntry}\n        onOpenChange={open => !open && handleCancelEdit()}\n      >\n        <DialogContent\n          className=\"!border-0 shadow-lg p-0\"\n          showCloseButton={false}\n        >\n          <DialogHeader>\n            <DialogTitle className=\"sr-only\">\n              {editingEntry?.type === 'replacement'\n                ? 'Edit replacement'\n                : 'Edit Dictionary Entry'}\n            </DialogTitle>\n          </DialogHeader>\n          <div className=\"px-6\">\n            <h2 className=\"text-lg font-semibold mb-4\">\n              {editingEntry?.type === 'replacement'\n                ? 'Edit replacement'\n                : 'Edit entry'}\n            </h2>\n\n            {editingEntry?.type === 'normal' ? (\n              <input\n                ref={editInputRef}\n                type=\"text\"\n                value={editContent}\n                onChange={e => setEditContent(e.target.value)}\n                onKeyDown={handleEditKeyDown}\n                className=\"w-full p-4 rounded-md resize-none focus:outline-none border border-neutral-200\"\n                placeholder=\"Enter dictionary entry...\"\n              />\n            ) : (\n              <div className=\"space-y-4\">\n                <div className=\"flex items-center gap-4\">\n                  <input\n                    ref={editFromRef}\n                    type=\"text\"\n                    value={editFrom}\n                    onChange={e => setEditFrom(e.target.value)}\n                    onKeyDown={handleEditKeyDown}\n                    className=\"flex-1 p-4 rounded-md resize-none focus:outline-none border border-neutral-200\"\n                    placeholder=\"Misspelling\"\n                  />\n                  <span className=\"text-gray-500\">→</span>\n                  <input\n                    type=\"text\"\n                    value={editTo}\n                    onChange={e => setEditTo(e.target.value)}\n                    onKeyDown={handleEditKeyDown}\n                    className=\"flex-1 p-4 rounded-md resize-none focus:outline-none border border-neutral-200\"\n                    placeholder=\"Correct spelling\"\n                  />\n                </div>\n              </div>\n            )}\n          </div>\n          <DialogFooter className=\"p-4\">\n            <Button\n              className=\"bg-neutral-200 hover:bg-neutral-300 text-black cursor-pointer\"\n              onClick={handleCancelEdit}\n            >\n              Cancel\n            </Button>\n            <Button\n              className=\"cursor-pointer\"\n              onClick={handleSaveEdit}\n              disabled={\n                editingEntry?.type === 'normal'\n                  ? !editContent.trim()\n                  : !editFrom.trim() || !editTo.trim()\n              }\n            >\n              Save\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n\n      {/* Add New Entry Dialog */}\n      <Dialog\n        open={showAddDialog}\n        onOpenChange={open => !open && handleCancelNew()}\n      >\n        <DialogContent\n          className=\"!border-0 shadow-lg p-0\"\n          showCloseButton={false}\n        >\n          <DialogHeader>\n            <DialogTitle className=\"sr-only\">Add to vocabulary</DialogTitle>\n          </DialogHeader>\n          <div className=\"px-6\">\n            <h2 className=\"text-lg font-semibold mb-4\">Add to vocabulary</h2>\n\n            <div className=\"flex items-center justify-between mb-4\">\n              <span className=\"text-sm font-medium\">Make it a replacement</span>\n              <Switch\n                checked={isReplacement}\n                onCheckedChange={handleReplacementToggle}\n              />\n            </div>\n\n            {!isReplacement ? (\n              <input\n                ref={addInputRef}\n                type=\"text\"\n                value={newEntryContent}\n                onChange={e => setNewEntryContent(e.target.value)}\n                onKeyDown={handleAddKeyDown}\n                className=\"w-full p-4 rounded-md resize-none focus:outline-none border border-neutral-200\"\n                placeholder=\"Enter dictionary entry...\"\n              />\n            ) : (\n              <div className=\"space-y-4\">\n                <div className=\"flex items-center gap-4\">\n                  <input\n                    ref={addFromRef}\n                    type=\"text\"\n                    value={newFrom}\n                    onChange={e => setNewFrom(e.target.value)}\n                    onKeyDown={handleAddKeyDown}\n                    className=\"flex-1 p-4 rounded-md resize-none focus:outline-none border border-neutral-200\"\n                    placeholder=\"Misspelling\"\n                  />\n                  <span className=\"text-gray-500\">→</span>\n                  <input\n                    type=\"text\"\n                    value={newTo}\n                    onChange={e => setNewTo(e.target.value)}\n                    onKeyDown={handleAddKeyDown}\n                    className=\"flex-1 p-4 rounded-md resize-none focus:outline-none border border-neutral-200\"\n                    placeholder=\"Correct spelling\"\n                  />\n                </div>\n              </div>\n            )}\n          </div>\n          <DialogFooter className=\"p-4\">\n            <Button\n              className=\"bg-neutral-200 hover:bg-neutral-300 text-black cursor-pointer\"\n              onClick={handleCancelNew}\n            >\n              Cancel\n            </Button>\n            <Button\n              className=\"cursor-pointer\"\n              onClick={handleSaveNew}\n              disabled={\n                isReplacement\n                  ? !newFrom.trim() || !newTo.trim()\n                  : !newEntryContent.trim()\n              }\n            >\n              Add word\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/home/contents/HomeContent.tsx",
    "content": "import React, { useCallback, useEffect, useState } from 'react'\nimport {\n  ChartNoAxesColumn,\n  InfoCircle,\n  Play,\n  Stop,\n  Copy,\n  Check,\n  Download,\n} from '@mynaui/icons-react'\nimport { EXTERNAL_LINKS } from '@/lib/constants/external-links'\nimport { useSettingsStore } from '../../../store/useSettingsStore'\nimport { Tooltip, TooltipTrigger, TooltipContent } from '../../ui/tooltip'\nimport { useAuthStore } from '@/app/store/useAuthStore'\nimport { Interaction } from '@/lib/main/sqlite/models'\nimport { TotalWordsIcon } from '../../icons/TotalWordsIcon'\nimport { SpeedIcon } from '../../icons/SpeedIcon'\nimport {\n  STREAK_MESSAGES,\n  SPEED_MESSAGES,\n  TOTAL_WORDS_MESSAGES,\n  getStreakLevel,\n  getSpeedLevel,\n  getTotalWordsLevel,\n  getActivityMessage,\n} from './activityMessages'\nimport { ItoMode } from '@/app/generated/ito_pb'\nimport { getKeyDisplay } from '@/app/utils/keyboard'\nimport { createStereo48kWavFromMonoPCM } from '@/app/utils/audioUtils'\nimport { KeyName } from '@/lib/types/keyboard'\nimport { usePlatform } from '@/app/hooks/usePlatform'\nimport { ProUpgradeDialog } from '../ProUpgradeDialog'\nimport useBillingState from '@/app/hooks/useBillingState'\n\n// Interface for interaction statistics\ninterface InteractionStats {\n  streakDays: number\n  totalWords: number\n  averageWPM: number\n}\n\nconst StatCard = ({\n  title,\n  value,\n  description,\n  icon,\n}: {\n  title: string\n  value: string\n  description: string\n  icon: React.ReactNode\n}) => {\n  return (\n    <div className=\"flex flex-col p-4 w-1/3 border-2 border-neutral-100 rounded-xl gap-4\">\n      <div className=\"flex flex-row items-center\">\n        <div className=\"flex flex-col gap-1\">\n          <div>{title}</div>\n          <div className=\"font-bold\">{value}</div>\n        </div>\n        <div className=\"flex flex-col items-end flex-1\">{icon}</div>\n      </div>\n      <div className=\"w-full text-neutral-400\">{description}</div>\n    </div>\n  )\n}\n\ninterface HomeContentProps {\n  isStartingTrial?: boolean\n}\n\nexport default function HomeContent({\n  isStartingTrial = false,\n}: HomeContentProps) {\n  const { getItoModeShortcuts } = useSettingsStore()\n  const keyboardShortcut = getItoModeShortcuts(ItoMode.TRANSCRIBE)[0].keys\n  const { user } = useAuthStore()\n  const firstName = user?.name?.split(' ')[0]\n  const platform = usePlatform()\n  const [interactions, setInteractions] = useState<Interaction[]>([])\n  const [loading, setLoading] = useState(true)\n  const [playingAudio, setPlayingAudio] = useState<string | null>(null)\n  const [audioInstances, setAudioInstances] = useState<\n    Map<string, HTMLAudioElement>\n  >(new Map())\n  const [copiedItems, setCopiedItems] = useState<Set<string>>(new Set())\n  const [openTooltipKey, setOpenTooltipKey] = useState<string | null>(null)\n  const [stats, setStats] = useState<InteractionStats>({\n    streakDays: 0,\n    totalWords: 0,\n    averageWPM: 0,\n  })\n  const [showProDialog, setShowProDialog] = useState(false)\n  const billingState = useBillingState()\n\n  // Persist \"has shown trial dialog\" flag in electron-store to survive remounts\n  const [hasShownTrialDialog, setHasShownTrialDialogState] = useState(() => {\n    try {\n      const authStore = window.electron?.store?.get('auth') || {}\n      const value = authStore?.hasShownTrialDialog === true\n      return value\n    } catch {\n      return false\n    }\n  })\n\n  const setHasShownTrialDialog = useCallback((value: boolean) => {\n    try {\n      setHasShownTrialDialogState(value)\n      window.api.send('electron-store-set', 'auth.hasShownTrialDialog', value)\n    } catch {\n      console.warn('Failed to persist hasShownTrialDialog flag')\n    }\n  }, [])\n\n  // Show trial dialog when trial starts\n  useEffect(() => {\n    if (\n      billingState.isTrialActive &&\n      billingState.proStatus === 'free_trial' &&\n      !hasShownTrialDialog &&\n      !billingState.isLoading\n    ) {\n      setShowProDialog(true)\n      setHasShownTrialDialog(true)\n    }\n  }, [\n    billingState.isTrialActive,\n    billingState.proStatus,\n    billingState.isLoading,\n    isStartingTrial,\n    hasShownTrialDialog,\n    setHasShownTrialDialog,\n  ])\n\n  // Listen for trial start event to refresh billing state\n  useEffect(() => {\n    const offTrialStarted = window.api.on('trial-started', async () => {\n      await billingState.refresh()\n    })\n\n    const offBillingSuccess = window.api.on(\n      'billing-session-completed',\n      async () => {\n        await billingState.refresh()\n      },\n    )\n\n    return () => {\n      offTrialStarted?.()\n      offBillingSuccess?.()\n    }\n  }, [billingState])\n\n  // Reset dialog flag when trial is no longer active or user becomes pro\n  // Only reset if we're certain the trial has ended (not just during loading/refreshing)\n  useEffect(() => {\n    if (billingState.isLoading) {\n      // Don't reset during loading to avoid race conditions\n      return\n    }\n\n    const shouldReset =\n      billingState.proStatus === 'active_pro' ||\n      (billingState.proStatus === 'none' && !billingState.isTrialActive)\n\n    if (shouldReset && hasShownTrialDialog) {\n      setHasShownTrialDialog(false)\n    }\n  }, [\n    billingState.proStatus,\n    billingState.isTrialActive,\n    billingState.isLoading,\n    hasShownTrialDialog,\n    setHasShownTrialDialog,\n  ])\n\n  // Calculate statistics from interactions\n  const calculateStats = useCallback(\n    (interactions: Interaction[]): InteractionStats => {\n      if (interactions.length === 0) {\n        return { streakDays: 0, totalWords: 0, averageWPM: 0 }\n      }\n\n      // Calculate streak (consecutive days with interactions)\n      const streakDays = calculateStreak(interactions)\n\n      // Calculate total words from transcripts\n      const totalWords = calculateTotalWords(interactions)\n\n      // Calculate average WPM (estimate based on average speaking rate)\n      const averageWPM = calculateAverageWPM(interactions)\n\n      return { streakDays, totalWords, averageWPM }\n    },\n    [],\n  )\n\n  const calculateStreak = (interactions: Interaction[]): number => {\n    if (interactions.length === 0) return 0\n\n    // Group interactions by date\n    const dateGroups = new Map<string, Interaction[]>()\n    interactions.forEach(interaction => {\n      const date = new Date(interaction.created_at).toDateString()\n      if (!dateGroups.has(date)) {\n        dateGroups.set(date, [])\n      }\n      dateGroups.get(date)!.push(interaction)\n    })\n\n    // Sort dates in descending order (most recent first)\n    const sortedDates = Array.from(dateGroups.keys()).sort(\n      (a, b) => new Date(b).getTime() - new Date(a).getTime(),\n    )\n\n    let streak = 0\n    const today = new Date()\n\n    for (let i = 0; i < sortedDates.length; i++) {\n      const currentDate = new Date(sortedDates[i])\n      const expectedDate = new Date(today)\n      expectedDate.setDate(today.getDate() - i)\n\n      // Check if current date matches expected date (allowing for today or previous consecutive days)\n      if (currentDate.toDateString() === expectedDate.toDateString()) {\n        streak++\n      } else {\n        break\n      }\n    }\n\n    return streak\n  }\n\n  const calculateTotalWords = (interactions: Interaction[]): number => {\n    return interactions.reduce((total, interaction) => {\n      const transcript = interaction.asr_output?.transcript?.trim()\n      if (transcript) {\n        // Count words by splitting on whitespace and filtering out empty strings\n        const words = transcript.split(/\\s+/).filter(word => word.length > 0)\n        return total + words.length\n      }\n      return total\n    }, 0)\n  }\n\n  const calculateAverageWPM = (interactions: Interaction[]): number => {\n    const validInteractions = interactions.filter(\n      interaction =>\n        interaction.asr_output?.transcript?.trim() && interaction.duration_ms,\n    )\n\n    if (validInteractions.length === 0) return 0\n\n    let totalWords = 0\n    let totalDurationMs = 0\n\n    validInteractions.forEach(interaction => {\n      const transcript = interaction.asr_output?.transcript?.trim()\n      if (transcript && interaction.duration_ms) {\n        // Count words by splitting on whitespace and filtering out empty strings\n        const words = transcript.split(/\\s+/).filter(word => word.length > 0)\n        totalWords += words.length\n        totalDurationMs += interaction.duration_ms\n      }\n    })\n\n    if (totalDurationMs === 0) return 0\n\n    // Calculate WPM: (total words / total duration in minutes)\n    const totalMinutes = totalDurationMs / (1000 * 60)\n    const wpm = totalWords / totalMinutes\n\n    // Round to nearest integer and ensure it's reasonable\n    return Math.round(Math.max(1, wpm))\n  }\n\n  const formatStreakText = (days: number): string => {\n    if (days === 0) return '0 days'\n    if (days === 1) return '1 day'\n    if (days < 7) return `${days} days`\n    if (days < 14) return '1 week'\n    if (days < 30) return `${Math.floor(days / 7)} weeks`\n    if (days < 60) return '1 month'\n    return `${Math.floor(days / 30)} months`\n  }\n\n  const loadInteractions = useCallback(async () => {\n    try {\n      const allInteractions = await window.api.interactions.getAll()\n\n      // Sort by creation date (newest first) - remove the slice(0, 10) to show all interactions\n      const sortedInteractions = allInteractions.sort(\n        (a: Interaction, b: Interaction) =>\n          new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),\n      )\n      setInteractions(sortedInteractions)\n\n      // Calculate and set statistics\n      const calculatedStats = calculateStats(sortedInteractions)\n      setStats(calculatedStats)\n    } catch (error) {\n      console.error('Failed to load interactions:', error)\n    } finally {\n      setLoading(false)\n    }\n  }, [calculateStats])\n\n  useEffect(() => {\n    loadInteractions()\n\n    // Listen for new interactions\n    const handleInteractionCreated = () => {\n      loadInteractions()\n    }\n\n    const unsubscribe = window.api.on(\n      'interaction-created',\n      handleInteractionCreated,\n    )\n\n    // Cleanup listener on unmount\n    return unsubscribe\n  }, [loadInteractions])\n\n  // Cleanup audio instances on unmount\n  useEffect(() => {\n    return () => {\n      audioInstances.forEach(audio => {\n        try {\n          audio.pause()\n          audio.currentTime = 0\n          // Best-effort release of object URL if used\n          if (audio.src?.startsWith('blob:')) {\n            URL.revokeObjectURL(audio.src)\n          }\n        } catch {\n          /* ignore */\n        }\n      })\n    }\n  }, [audioInstances])\n\n  const formatTime = (dateString: string) => {\n    const date = new Date(dateString)\n    return date.toLocaleString('en-US', {\n      hour: 'numeric',\n      minute: '2-digit',\n      hour12: true,\n    })\n  }\n\n  const formatDate = (dateString: string) => {\n    const date = new Date(dateString)\n    const today = new Date()\n    const yesterday = new Date()\n    yesterday.setDate(today.getDate() - 1)\n\n    const isToday = date.toDateString() === today.toDateString()\n    const isYesterday = date.toDateString() === yesterday.toDateString()\n\n    if (isToday) return 'TODAY'\n    if (isYesterday) return 'YESTERDAY'\n\n    return date\n      .toLocaleDateString('en-US', {\n        weekday: 'long',\n        month: 'short',\n        day: 'numeric',\n      })\n      .toUpperCase()\n  }\n\n  const groupInteractionsByDate = (interactions: Interaction[]) => {\n    const groups: { [key: string]: Interaction[] } = {}\n\n    interactions.forEach(interaction => {\n      const dateKey = formatDate(interaction.created_at)\n      if (!groups[dateKey]) {\n        groups[dateKey] = []\n      }\n      groups[dateKey].push(interaction)\n    })\n\n    return groups\n  }\n\n  const getDisplayText = (interaction: Interaction) => {\n    // Check for errors first\n    if (interaction.asr_output?.error) {\n      // Prefer precise error code mapping when available\n      const code = interaction.asr_output?.errorCode\n      if (code === 'CLIENT_TRANSCRIPTION_QUALITY_ERROR') {\n        return {\n          text: 'Audio quality too low',\n          isError: true,\n          tooltip:\n            'Audio quality was too low to generate a reliable transcript',\n        }\n      }\n      if (\n        interaction.asr_output.error.includes('No speech detected in audio.') ||\n        interaction.asr_output.error.includes('Unable to transcribe audio.')\n      ) {\n        return {\n          text: 'Audio is silent',\n          isError: true,\n          tooltip: \"Ito didn't detect any words so the transcript is empty\",\n        }\n      }\n      return {\n        text: 'Transcription failed',\n        isError: true,\n        tooltip: interaction.asr_output.error,\n      }\n    }\n\n    // Check for empty transcript\n    const transcript = interaction.asr_output?.transcript?.trim()\n\n    if (!transcript) {\n      return {\n        text: 'Audio is silent.',\n        isError: true,\n        tooltip: \"Ito didn't detect any words so the transcript is empty\",\n      }\n    }\n\n    // Return the actual transcript\n    return {\n      text: transcript,\n      isError: false,\n      tooltip: null,\n    }\n  }\n\n  const handleAudioPlayStop = async (interaction: Interaction) => {\n    try {\n      // If this interaction is currently playing, stop it\n      if (playingAudio === interaction.id) {\n        const current = audioInstances.get(interaction.id)\n        if (current) {\n          current.pause()\n          current.currentTime = 0\n          if (current.src?.startsWith('blob:')) {\n            URL.revokeObjectURL(current.src)\n          }\n        }\n        setPlayingAudio(null)\n        return\n      }\n\n      // Stop any other playing audio\n      if (playingAudio) {\n        const other = audioInstances.get(playingAudio)\n        if (other) {\n          other.pause()\n          other.currentTime = 0\n          if (other.src?.startsWith('blob:')) {\n            URL.revokeObjectURL(other.src)\n          }\n        }\n      }\n\n      if (!interaction.raw_audio) {\n        console.warn('No audio data available for this interaction')\n        return\n      }\n\n      // Set playing state immediately for responsive UI\n      setPlayingAudio(interaction.id)\n\n      // Reuse existing audio instance if available\n      let audio = audioInstances.get(interaction.id)\n\n      if (!audio) {\n        const pcmData = new Uint8Array(interaction.raw_audio)\n        try {\n          // Convert raw PCM (mono, typically 16 kHz) to 48 kHz stereo WAV for smoother playback\n          const wavBuffer = createStereo48kWavFromMonoPCM(\n            pcmData,\n            interaction.sample_rate || 16000,\n            48000,\n          )\n          const audioBlob = new Blob([wavBuffer], { type: 'audio/wav' })\n          const audioUrl = URL.createObjectURL(audioBlob)\n\n          audio = new Audio(audioUrl)\n          audio.onended = () => {\n            setPlayingAudio(null)\n            if (audio && audio.src?.startsWith('blob:')) {\n              URL.revokeObjectURL(audio.src)\n            }\n          }\n          audio.onerror = err => {\n            console.error('Audio playback error:', err)\n            setPlayingAudio(null)\n            if (audio && audio.src?.startsWith('blob:')) {\n              URL.revokeObjectURL(audio.src)\n            }\n          }\n\n          setAudioInstances(prev => new Map(prev).set(interaction.id, audio!))\n        } catch (error) {\n          console.error('Failed to create audio instance:', error)\n          setPlayingAudio(null)\n          return\n        }\n      }\n\n      try {\n        await audio.play()\n      } catch (playError) {\n        console.error('Failed to start audio playback:', playError)\n        setPlayingAudio(null)\n      }\n    } catch (error) {\n      console.error('Failed to play/stop audio:', error)\n      setPlayingAudio(null)\n    }\n  }\n\n  const groupedInteractions = groupInteractionsByDate(interactions)\n\n  const copyToClipboard = async (text: string, interactionId: string) => {\n    try {\n      await navigator.clipboard.writeText(text)\n      setCopiedItems(prev => new Set(prev).add(interactionId))\n      setOpenTooltipKey(`copy:${interactionId}`) // Keep tooltip open\n\n      // Reset the copied state after 2 seconds\n      setTimeout(() => {\n        setCopiedItems(prev => {\n          const newSet = new Set(prev)\n          newSet.delete(interactionId)\n          return newSet\n        })\n        // Close tooltip if it's still open for this item (do not override if user hovered elsewhere)\n        setOpenTooltipKey(prev =>\n          prev === `copy:${interactionId}` ? null : prev,\n        )\n      }, 2000)\n    } catch (error) {\n      console.error('Failed to copy text:', error)\n    }\n  }\n\n  const handleAudioDownload = async (interaction: Interaction) => {\n    try {\n      if (!interaction.raw_audio) {\n        console.warn('No audio data available for download')\n        return\n      }\n\n      const pcmData = new Uint8Array(interaction.raw_audio)\n      // Convert raw PCM to WAV format\n      const wavBuffer = createStereo48kWavFromMonoPCM(\n        pcmData,\n        interaction.sample_rate || 16000,\n        48000,\n      )\n      const audioBlob = new Blob([wavBuffer], { type: 'audio/wav' })\n      const audioUrl = URL.createObjectURL(audioBlob)\n\n      // Format filename with timestamp (YYYYMMDD_HHMMSS)\n      const date = new Date(interaction.created_at)\n      const timestamp = date\n        .toISOString()\n        .replace(/[-:]/g, '')\n        .replace('T', '_')\n        .slice(0, 15)\n      const filename = `ito-recording-${timestamp}.wav`\n\n      // Create temporary link and trigger download\n      const link = document.createElement('a')\n      link.href = audioUrl\n      link.download = filename\n      document.body.appendChild(link)\n      link.click()\n      document.body.removeChild(link)\n\n      // Clean up the blob URL\n      URL.revokeObjectURL(audioUrl)\n    } catch (error) {\n      console.error('Failed to download audio:', error)\n    }\n  }\n\n  return (\n    <div className=\"w-full h-full flex flex-col\">\n      {/* Fixed Header Content */}\n      <div className=\"flex-shrink-0 px-24\">\n        <div className=\"flex items-center justify-between mb-8\">\n          <div>\n            <h1 className=\"text-2xl font-medium\">\n              Welcome back{firstName ? `, ${firstName}!` : '!'}\n            </h1>\n          </div>\n        </div>\n        <div className=\"flex gap-4 w-full mb-6\">\n          <div className=\"flex w-full items-center text-sm text-gray-700 gap-2\">\n            <StatCard\n              title=\"Weekly Streak\"\n              value={formatStreakText(stats.streakDays)}\n              description={getActivityMessage(\n                STREAK_MESSAGES,\n                getStreakLevel(stats.streakDays),\n              )}\n              icon={\n                <div className=\"p-2 bg-blue-50 rounded-md\">\n                  <ChartNoAxesColumn\n                    className=\"w-6 h-6 text-blue-400 border-2 p-1 rounded-full\"\n                    strokeWidth={4}\n                  />\n                </div>\n              }\n            />\n            <StatCard\n              title=\"Average Speed\"\n              value={`${stats.averageWPM} words / minute`}\n              description={getActivityMessage(\n                SPEED_MESSAGES,\n                getSpeedLevel(stats.averageWPM),\n              )}\n              icon={\n                <div className=\"p-2 bg-green-50 rounded-md\">\n                  <SpeedIcon />\n                </div>\n              }\n            />\n            <StatCard\n              title=\"Total Words\"\n              value={`${stats.totalWords} ${stats.totalWords === 1 ? 'word' : 'words'}`}\n              description={getActivityMessage(\n                TOTAL_WORDS_MESSAGES,\n                getTotalWordsLevel(stats.totalWords),\n              )}\n              icon={\n                <div className=\"p-2 bg-orange-50 rounded-md\">\n                  <TotalWordsIcon />\n                </div>\n              }\n            />\n          </div>\n        </div>\n\n        {/* Dictation Info Box */}\n        <div className=\"bg-slate-100 rounded-xl p-6 flex items-center justify-between mb-10\">\n          <div>\n            <div className=\"text-base font-medium mb-1\">\n              Voice dictation in any app\n            </div>\n            <div className=\"text-sm text-gray-600\">\n              <span key=\"hold-down\">Hold down the trigger key </span>\n              {keyboardShortcut.map((key, index) => (\n                <React.Fragment key={index}>\n                  <span className=\"bg-slate-50 px-1 py-0.5 rounded text-xs font-mono shadow-sm\">\n                    {getKeyDisplay(key as KeyName, platform, {\n                      showDirectionalText: false,\n                      format: 'label',\n                    })}\n                  </span>\n                  <span>{index < keyboardShortcut.length - 1 && ' + '}</span>\n                </React.Fragment>\n              ))}\n              <span key=\"and\"> and speak into any textbox</span>\n            </div>\n          </div>\n          <button\n            className=\"bg-gray-900 text-white px-6 py-3 rounded-full font-semibold hover:bg-gray-800 cursor-pointer\"\n            onClick={() =>\n              window.api?.invoke('web-open-url', EXTERNAL_LINKS.WEBSITE)\n            }\n          >\n            Explore use cases\n          </button>\n        </div>\n\n        {/* Recent Activity Header */}\n        <div className=\"text-sm text-muted-foreground mb-6\">\n          Recent activity\n        </div>\n      </div>\n\n      {/* Scrollable Recent Activity Section */}\n      <div className=\"flex-1 px-24 overflow-y-auto scrollbar-hide\">\n        {loading ? (\n          <div className=\"bg-white rounded-lg border border-slate-200 p-8 text-center text-gray-500\">\n            Loading recent activity...\n          </div>\n        ) : interactions.length === 0 ? (\n          <div className=\"bg-white rounded-lg border border-slate-200 p-8 text-center text-gray-500\">\n            <p className=\"text-sm\">No interactions yet</p>\n            <p className=\"text-xs mt-1\">\n              Try using voice dictation by pressing{' '}\n              {keyboardShortcut.join(' + ')}\n            </p>\n          </div>\n        ) : (\n          Object.entries(groupedInteractions).map(\n            ([dateLabel, dateInteractions]) => (\n              <div key={dateLabel} className=\"mb-6\">\n                <div className=\"text-xs text-gray-500 mb-4\">{dateLabel}</div>\n                <div className=\"bg-white rounded-lg border border-slate-200 divide-y divide-slate-200\">\n                  {dateInteractions.map(interaction => {\n                    const displayInfo = getDisplayText(interaction)\n\n                    return (\n                      <div\n                        key={interaction.id}\n                        className=\"flex items-center justify-between px-4 py-4 gap-10 hover:bg-gray-50 transition-colors duration-200 group\"\n                      >\n                        <div className=\"flex items-center gap-10\">\n                          <div className=\"text-gray-600 min-w-[60px]\">\n                            {formatTime(interaction.created_at)}\n                          </div>\n                          <div\n                            className={`${displayInfo.isError ? 'text-gray-600' : 'text-gray-900'} flex items-center gap-1`}\n                          >\n                            {displayInfo.text}\n                            {displayInfo.tooltip && (\n                              <Tooltip>\n                                <TooltipTrigger>\n                                  <InfoCircle className=\"w-4 h-4 text-gray-400\" />\n                                </TooltipTrigger>\n                                <TooltipContent>\n                                  {displayInfo.tooltip}\n                                </TooltipContent>\n                              </Tooltip>\n                            )}\n                          </div>\n                        </div>\n\n                        {/* Copy, Download, and Play buttons - only show on hover or when playing */}\n                        <div\n                          className={`flex items-center gap-2 ${playingAudio === interaction.id ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} transition-opacity duration-200`}\n                        >\n                          {/* Copy button */}\n                          {!displayInfo.isError && (\n                            <Tooltip\n                              open={openTooltipKey === `copy:${interaction.id}`}\n                              onOpenChange={open => {\n                                if (open) {\n                                  // Opening: exclusively show this tooltip\n                                  setOpenTooltipKey(`copy:${interaction.id}`)\n                                } else {\n                                  // Closing: if in copied state, keep it open until timer clears,\n                                  // otherwise close normally\n                                  if (!copiedItems.has(interaction.id)) {\n                                    setOpenTooltipKey(prev =>\n                                      prev === `copy:${interaction.id}`\n                                        ? null\n                                        : prev,\n                                    )\n                                  }\n                                }\n                              }}\n                            >\n                              <TooltipTrigger asChild>\n                                <button\n                                  className={`p-1.5 hover:bg-gray-200 rounded transition-colors cursor-pointer ${\n                                    copiedItems.has(interaction.id)\n                                      ? 'text-green-600'\n                                      : 'text-gray-600'\n                                  }`}\n                                  onClick={() =>\n                                    copyToClipboard(\n                                      displayInfo.text,\n                                      interaction.id,\n                                    )\n                                  }\n                                >\n                                  {copiedItems.has(interaction.id) ? (\n                                    <Check className=\"w-4 h-4\" />\n                                  ) : (\n                                    <Copy className=\"w-4 h-4\" />\n                                  )}\n                                </button>\n                              </TooltipTrigger>\n                              <TooltipContent side=\"top\" sideOffset={5}>\n                                {copiedItems.has(interaction.id)\n                                  ? 'Copied 🎉'\n                                  : 'Copy'}\n                              </TooltipContent>\n                            </Tooltip>\n                          )}\n\n                          {/* Download button */}\n                          {interaction.raw_audio && (\n                            <Tooltip\n                              open={\n                                openTooltipKey === `download:${interaction.id}`\n                              }\n                              onOpenChange={open => {\n                                setOpenTooltipKey(\n                                  open ? `download:${interaction.id}` : null,\n                                )\n                              }}\n                            >\n                              <TooltipTrigger asChild>\n                                <button\n                                  className=\"p-1.5 hover:bg-gray-200 rounded transition-colors cursor-pointer text-gray-600\"\n                                  onClick={() =>\n                                    handleAudioDownload(interaction)\n                                  }\n                                >\n                                  <Download className=\"w-4 h-4\" />\n                                </button>\n                              </TooltipTrigger>\n                              <TooltipContent side=\"top\" sideOffset={5}>\n                                Download audio\n                              </TooltipContent>\n                            </Tooltip>\n                          )}\n\n                          {/* Play/Stop button with tooltip */}\n                          <Tooltip\n                            open={openTooltipKey === `play:${interaction.id}`}\n                            onOpenChange={open => {\n                              setOpenTooltipKey(\n                                open ? `play:${interaction.id}` : null,\n                              )\n                            }}\n                          >\n                            <TooltipTrigger asChild>\n                              <button\n                                className={`p-1.5 hover:bg-gray-200 rounded transition-colors cursor-pointer ${\n                                  playingAudio === interaction.id\n                                    ? 'bg-blue-50 text-blue-600'\n                                    : 'text-gray-600'\n                                }`}\n                                onClick={() => handleAudioPlayStop(interaction)}\n                                disabled={!interaction.raw_audio}\n                              >\n                                {playingAudio === interaction.id ? (\n                                  <Stop className=\"w-4 h-4\" />\n                                ) : (\n                                  <Play className=\"w-4 h-4\" />\n                                )}\n                              </button>\n                            </TooltipTrigger>\n                            <TooltipContent side=\"top\" sideOffset={5}>\n                              {!interaction.raw_audio\n                                ? 'No audio available'\n                                : playingAudio === interaction.id\n                                  ? 'Stop'\n                                  : 'Play'}\n                            </TooltipContent>\n                          </Tooltip>\n                        </div>\n                      </div>\n                    )\n                  })}\n                </div>\n              </div>\n            ),\n          )\n        )}\n      </div>\n\n      {/* Pro Upgrade Dialog */}\n      <ProUpgradeDialog open={showProDialog} onOpenChange={setShowProDialog} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/home/contents/NotesContent.tsx",
    "content": "import { useEffect, useRef, useState } from 'react'\nimport { useNotesStore } from '../../../store/useNotesStore'\nimport { useSettingsStore } from '../../../store/useSettingsStore'\nimport Masonry from '@mui/lab/Masonry'\nimport { AudioIcon } from '../../icons/AudioIcon'\nimport { ArrowUp, Grid, Rows, Search, X } from '@mynaui/icons-react'\nimport { Note } from '../../ui/note'\nimport { StatusIndicator } from '../../ui/status-indicator'\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogFooter,\n} from '../../ui/dialog'\nimport { Button } from '../../ui/button'\nimport { ItoMode } from '@/app/generated/ito_pb'\nimport { getKeyDisplayInfo } from '@/lib/types/keyboard'\nimport { usePlatform } from '@/app/hooks/usePlatform'\n\nexport default function NotesContent() {\n  const { notes, loadNotes, addNote, deleteNote, updateNote } = useNotesStore()\n  const { getItoModeShortcuts } = useSettingsStore()\n  const keyboardShortcut = getItoModeShortcuts(ItoMode.TRANSCRIBE)[0].keys\n  const [creatingNote, setCreatingNote] = useState(false)\n  const [showAddNoteButton, setShowAddNoteButton] = useState(false)\n  const [noteContent, setNoteContent] = useState('')\n  const [showScrollToTop, setShowScrollToTop] = useState(false)\n  const [containerHeight, setContainerHeight] = useState(128) // 128px = h-32\n  const [showSearch, setShowSearch] = useState(false)\n  const [searchQuery, setSearchQuery] = useState('')\n  const [showDropdown, setShowDropdown] = useState<number | null>(null)\n  const [statusIndicator, setStatusIndicator] = useState<\n    'success' | 'error' | null\n  >(null)\n  const [statusMessage, setStatusMessage] = useState<string>('')\n  const textareaRef = useRef<HTMLTextAreaElement>(null)\n  const searchInputRef = useRef<HTMLInputElement>(null)\n  const containerRef = useRef<HTMLDivElement>(null)\n  const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')\n  const [editingNote, setEditingNote] = useState<{\n    id: string\n    content: string\n  } | null>(null)\n  const [editContent, setEditContent] = useState('')\n  const editTextareaRef = useRef<HTMLTextAreaElement>(null)\n  const platform = usePlatform()\n\n  useEffect(() => {\n    loadNotes()\n  }, [loadNotes, addNote, notes.length])\n\n  const formatDate = (date: Date) => {\n    return date.toLocaleDateString('en-US', {\n      month: 'short',\n      day: 'numeric',\n    })\n  }\n\n  const formatTime = (date: Date) => {\n    return date.toLocaleString('en-US', {\n      hour: 'numeric',\n      minute: '2-digit',\n      hour12: true,\n    })\n  }\n\n  const truncateContent = (content: string, maxLength: number = 100) => {\n    if (content.length <= maxLength) {\n      return content\n    }\n    return content.slice(0, maxLength) + '...'\n  }\n\n  const handleBlur = () => {\n    // If the note isn't empty, don't close the input\n    setTimeout(() => {\n      if (textareaRef.current?.value.trim() === '') {\n        setCreatingNote(false)\n      }\n    }, 200)\n  }\n\n  const updateNoteContent = (content: string) => {\n    setNoteContent(content)\n    const fmt = new Intl.DateTimeFormat('en-GB', {\n      hour: '2-digit',\n      minute: '2-digit',\n      second: '2-digit',\n      fractionalSecondDigits: 3,\n      hour12: false,\n    })\n\n    const timestamp = fmt.format(new Date())\n    console.log(`${timestamp}: Pasted content: ${content}`)\n    if (content.trim() !== '') {\n      setShowAddNoteButton(true)\n    } else {\n      setShowAddNoteButton(false)\n    }\n\n    // Auto-resize textarea and container\n    if (textareaRef.current) {\n      textareaRef.current.style.height = 'auto'\n      const scrollHeight = textareaRef.current.scrollHeight\n      textareaRef.current.style.height = `${scrollHeight}px`\n\n      // Calculate container height: textarea height + padding + button space\n      const minHeight = 192 // min-h-48 = 192px\n      const paddingAndButton = 48 + 40 // 48px padding + 40px for button space\n      const newContainerHeight = Math.max(\n        minHeight,\n        scrollHeight + paddingAndButton,\n      )\n      setContainerHeight(newContainerHeight)\n    }\n  }\n\n  const toggleViewMode = () => {\n    setViewMode(viewMode === 'grid' ? 'list' : 'grid')\n  }\n\n  const openSearch = () => {\n    setShowSearch(true)\n    // Focus the search input after the component updates\n    setTimeout(() => {\n      searchInputRef.current?.focus()\n    }, 100)\n  }\n\n  const closeSearch = () => {\n    setShowSearch(false)\n    setSearchQuery('')\n  }\n\n  // Filter notes based on search query\n  const filteredNotes =\n    searchQuery.trim() === ''\n      ? notes\n      : notes.filter(note =>\n          note.content.toLowerCase().includes(searchQuery.toLowerCase()),\n        )\n\n  const handleAddNote = async () => {\n    if (noteContent.trim() !== '') {\n      try {\n        await addNote(noteContent.trim())\n        setNoteContent('')\n        setCreatingNote(false)\n        setShowAddNoteButton(false)\n        setStatusMessage('Note saved')\n        setStatusIndicator('success')\n      } catch (error) {\n        console.error('Failed to add note:', error)\n        setStatusMessage('Failed to save note')\n        setStatusIndicator('error')\n      }\n    }\n  }\n\n  const handleCopyToClipboard = async (content: string) => {\n    try {\n      await navigator.clipboard.writeText(content)\n      setShowDropdown(null)\n      // You could add a toast notification here\n    } catch (err) {\n      console.error('Failed to copy text: ', err)\n    }\n  }\n\n  const handleDeleteNote = async (noteId: string) => {\n    try {\n      await deleteNote(noteId)\n      setShowDropdown(null)\n      setStatusMessage('Deleted note')\n      setStatusIndicator('success')\n    } catch (error) {\n      console.error('Failed to delete note:', error)\n      setStatusMessage('Failed to delete note')\n      setStatusIndicator('error')\n    }\n  }\n\n  const handleEditNote = (noteId: string) => {\n    const note = notes.find(n => n.id === noteId)\n    if (note) {\n      setEditingNote({ id: noteId, content: note.content })\n      setEditContent(note.content)\n      setShowDropdown(null)\n      // Focus the textarea after the dialog opens\n      setTimeout(() => {\n        editTextareaRef.current?.focus()\n      }, 100)\n    }\n  }\n\n  const handleSaveEdit = async () => {\n    if (editingNote && editContent.trim() !== '') {\n      try {\n        await updateNote(editingNote.id, editContent.trim())\n        setEditingNote(null)\n        setEditContent('')\n        setStatusMessage('Updated note')\n        setStatusIndicator('success')\n      } catch (error) {\n        console.error('Failed to update note:', error)\n        setStatusMessage('Failed to update note')\n        setStatusIndicator('error')\n      }\n    }\n  }\n\n  const handleCancelEdit = () => {\n    setEditingNote(null)\n    setEditContent('')\n  }\n\n  // Handle keyboard shortcuts in edit dialog\n  const handleEditKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n    if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {\n      e.preventDefault()\n      handleSaveEdit()\n    } else if (e.key === 'Escape') {\n      e.preventDefault()\n      handleCancelEdit()\n    }\n  }\n\n  const toggleDropdown = (index: number, e: React.MouseEvent) => {\n    e.stopPropagation()\n    setShowDropdown(showDropdown === index ? null : index)\n  }\n\n  // Auto-resize on mount and when creatingNote changes\n  useEffect(() => {\n    if (creatingNote && textareaRef.current) {\n      textareaRef.current.style.height = 'auto'\n      const scrollHeight = textareaRef.current.scrollHeight\n      textareaRef.current.style.height = `${scrollHeight}px`\n\n      // Set container height for creating state\n      const minHeight = 192 // min-h-48 = 192px\n      const paddingAndButton = 48 // 48px padding for button space\n      const newContainerHeight = Math.max(\n        minHeight,\n        scrollHeight + paddingAndButton,\n      )\n      setContainerHeight(newContainerHeight)\n    } else if (!creatingNote) {\n      // Reset to default height when not creating\n      setContainerHeight(128) // h-32 = 128px\n      if (textareaRef.current) {\n        textareaRef.current.style.height = ''\n      }\n    }\n  }, [creatingNote])\n\n  // Handle scroll events\n  useEffect(() => {\n    const handleScroll = () => {\n      if (containerRef.current) {\n        const scrollTop = containerRef.current.scrollTop\n        setShowScrollToTop(scrollTop > 200) // Show button after scrolling 200px\n      }\n    }\n\n    const container = containerRef.current\n    if (container) {\n      container.addEventListener('scroll', handleScroll)\n      return () => container.removeEventListener('scroll', handleScroll)\n    }\n\n    return () => {}\n  }, [])\n\n  // Handle escape key for closing search\n  useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.key === 'Escape' && showSearch) {\n        closeSearch()\n      }\n    }\n\n    if (showSearch) {\n      document.addEventListener('keydown', handleKeyDown)\n      return () => document.removeEventListener('keydown', handleKeyDown)\n    }\n\n    return () => {}\n  }, [showSearch])\n\n  // Handle clicks outside dropdown to close it\n  useEffect(() => {\n    const handleClickOutside = () => {\n      setShowDropdown(null)\n    }\n\n    if (showDropdown !== null) {\n      document.addEventListener('click', handleClickOutside)\n      return () => document.removeEventListener('click', handleClickOutside)\n    }\n\n    return () => {}\n  }, [showDropdown])\n\n  const scrollToTop = () => {\n    if (containerRef.current) {\n      containerRef.current.scrollTo({\n        top: 0,\n        behavior: 'smooth',\n      })\n    }\n  }\n\n  return (\n    <div\n      ref={containerRef}\n      className=\"w-full max-w-6xl mx-auto px-4 h-200 overflow-y-auto relative px-24\"\n      style={{\n        height: '640px',\n        msOverflowStyle: 'none' /* Internet Explorer 10+ */,\n        scrollbarWidth: 'none' /* Firefox */,\n      }}\n    >\n      {/* Header */}\n      {showSearch ? (\n        <div className=\"flex items-center gap-4 mb-8 px-4 py-2 bg-white border border-gray-200 rounded-lg\">\n          <Search className=\"w-5 h-5 text-gray-400 flex-shrink-0\" />\n          <input\n            ref={searchInputRef}\n            type=\"text\"\n            value={searchQuery}\n            onChange={e => setSearchQuery(e.target.value)}\n            placeholder=\"Search your notes\"\n            className=\"flex-1 text-sm outline-none placeholder-gray-400\"\n          />\n          <button\n            onClick={closeSearch}\n            className=\"p-1 hover:bg-gray-100 rounded transition-colors flex-shrink-0\"\n            title=\"Close search\"\n          >\n            <X className=\"w-5 h-5 text-gray-400\" />\n          </button>\n        </div>\n      ) : (\n        <div className=\"flex items-center justify-between mb-8\">\n          <h1 className=\"text-xl font-medium text-gray-900 w-full text-center\">\n            What's on your mind today?\n          </h1>\n        </div>\n      )}\n\n      {/* Text Input Area - Only show when not searching */}\n      {!showSearch && (\n        <div\n          className=\"shadow-lg rounded-2xl mb-8 border border-gray-200 w-3/5 mx-auto transition-all duration-200 ease-in-out relative\"\n          style={{ height: `${containerHeight}px` }}\n        >\n          {!creatingNote && (\n            <div className=\"absolute top-6 left-6 flex items-center gap-1 text-gray-500 pointer-events-none\">\n              <AudioIcon />\n              <span>Take a quick note with your voice</span>\n            </div>\n          )}\n          <textarea\n            ref={textareaRef}\n            className={`w-full pt-6 px-6 focus:outline-none resize-none overflow-hidden ${creatingNote ? 'cursor-text' : 'cursor-pointer'}`}\n            value={noteContent}\n            onChange={e => updateNoteContent(e.target.value)}\n            onClick={() => setCreatingNote(true)}\n            onBlur={handleBlur}\n            placeholder={`${creatingNote ? `Press and hold ${keyboardShortcut.map(k => getKeyDisplayInfo(k, platform).label).join(' + ')} and start speaking` : ''}`}\n          />\n          {showAddNoteButton && (\n            <div className=\"absolute bottom-3 right-3\">\n              <button\n                onClick={handleAddNote}\n                className=\"bg-neutral-200 px-4 py-2 rounded-md font-semibold hover:bg-neutral-300 cursor-pointer\"\n              >\n                Add note\n              </button>\n            </div>\n          )}\n        </div>\n      )}\n      <div\n        className={`${viewMode === 'grid' || showSearch ? '' : 'm-auto w-3/5'}`}\n      >\n        <div className=\"flex items-center justify-between mb-1\">\n          <div className=\"text-xs text-gray-500 font-medium uppercase tracking-wide\">\n            {showSearch\n              ? `Search Results (${filteredNotes.length})`\n              : `Notes (${notes.length})`}\n          </div>\n          <div className=\"flex items-center gap-1\">\n            <button\n              className=\"p-2 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer\"\n              title=\"Search\"\n              onClick={openSearch}\n            >\n              <Search className=\"w-5 h-5 text-neutral-400\" />\n            </button>\n            <button\n              className=\"p-2 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer\"\n              title=\"List view\"\n              onClick={toggleViewMode}\n            >\n              {viewMode === 'grid' ? (\n                <Rows className=\"w-5 h-5 text-neutral-400\" />\n              ) : (\n                <Grid className=\"w-5 h-5 text-neutral-400\" />\n              )}\n            </button>\n          </div>\n        </div>\n        <div className=\"w-full h-[1px] bg-slate-200 mb-4\"></div>\n        {/* Notes Masonry Layout */}\n        {(showSearch ? filteredNotes.length === 0 : notes.length === 0) ? (\n          <div className=\"py-4 text-gray-500\">\n            {showSearch ? (\n              <>\n                <p className=\"text-sm\">No notes found</p>\n                <p className=\"text-xs mt-1\">Try a different search term</p>\n              </>\n            ) : (\n              <>\n                <p className=\"text-sm\">No notes yet</p>\n              </>\n            )}\n          </div>\n        ) : (\n          <div className=\"py-4\">\n            {viewMode === 'grid' && (\n              <Masonry columns={{ xs: 1, sm: 2, md: 3 }} spacing={2}>\n                {(showSearch ? filteredNotes : notes).map((note, index) => (\n                  <Note\n                    key={note.id}\n                    note={note}\n                    index={index}\n                    showDropdown={showDropdown}\n                    onEdit={handleEditNote}\n                    onToggleDropdown={toggleDropdown}\n                    onCopyToClipboard={handleCopyToClipboard}\n                    onDeleteNote={handleDeleteNote}\n                    formatDate={formatDate}\n                    formatTime={formatTime}\n                    truncateContent={truncateContent}\n                    searchQuery={showSearch ? searchQuery : undefined}\n                  />\n                ))}\n              </Masonry>\n            )}\n            {viewMode === 'list' && (\n              <div className=\"flex flex-col gap-4\">\n                {(showSearch ? filteredNotes : notes).map((note, index) => (\n                  <Note\n                    key={note.id}\n                    note={note}\n                    index={index}\n                    showDropdown={showDropdown}\n                    onEdit={handleEditNote}\n                    onToggleDropdown={toggleDropdown}\n                    onCopyToClipboard={handleCopyToClipboard}\n                    onDeleteNote={handleDeleteNote}\n                    formatDate={formatDate}\n                    formatTime={formatTime}\n                    truncateContent={truncateContent}\n                    searchQuery={showSearch ? searchQuery : undefined}\n                  />\n                ))}\n              </div>\n            )}\n          </div>\n        )}\n      </div>\n\n      {/* Scroll to Top Button */}\n      {showScrollToTop && (\n        <button\n          onClick={scrollToTop}\n          className=\"fixed bottom-8 bg-black text-white right-8 w-8 h-8 rounded-full shadow-lg hover:shadow-xl hover:-translate-y-1 transition-all duration-200 flex items-center justify-center group z-50 cursor-pointer\"\n          aria-label=\"Scroll to top\"\n        >\n          <ArrowUp className=\"w-4 h-4 font-bold\" />\n        </button>\n      )}\n\n      {/* Edit Note Dialog */}\n      <Dialog\n        open={!!editingNote}\n        onOpenChange={open => !open && handleCancelEdit()}\n      >\n        <DialogContent\n          className=\"!border-0 shadow-lg p-0\"\n          showCloseButton={false}\n        >\n          <DialogHeader>\n            <DialogTitle className=\"sr-only\">Edit Note</DialogTitle>\n          </DialogHeader>\n          <div>\n            <textarea\n              ref={editTextareaRef}\n              value={editContent}\n              onChange={e => setEditContent(e.target.value)}\n              onKeyDown={handleEditKeyDown}\n              className=\"w-full px-4 rounded-md resize-none focus:outline-none border-0\"\n              rows={6}\n              placeholder=\"Edit your note...\"\n            />\n          </div>\n          <DialogFooter className=\"p-4\">\n            <Button\n              className=\"bg-neutral-200 hover:bg-neutral-300 text-black cursor-pointer\"\n              onClick={handleCancelEdit}\n            >\n              Cancel\n            </Button>\n            <Button\n              className=\"cursor-pointer\"\n              onClick={handleSaveEdit}\n              disabled={!editContent.trim()}\n            >\n              Save\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n\n      {/* Status Indicator */}\n      <StatusIndicator\n        status={statusIndicator}\n        onHide={() => {\n          setStatusIndicator(null)\n          setStatusMessage('')\n        }}\n        successMessage={statusMessage}\n        errorMessage={statusMessage}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/home/contents/SettingsContent.tsx",
    "content": "import { useMainStore } from '@/app/store/useMainStore'\nimport GeneralSettingsContent from './settings/GeneralSettingsContent'\nimport AudioSettingsContent from './settings/AudioSettingsContent'\nimport AccountSettingsContent from './settings/AccountSettingsContent'\nimport KeyboardSettingsContent from './settings/KeyboardSettingsContent'\nimport AdvancedSettingsContent from './settings/AdvancedSettingsContent'\nimport PricingBillingSettingsContent from './settings/PricingBillingSettingsContent'\n\nexport default function SettingsContent() {\n  const { settingsPage, setSettingsPage } = useMainStore()\n\n  const settingsMenuItems = [\n    { id: 'general', label: 'General', active: settingsPage === 'general' },\n    { id: 'keyboard', label: 'Keyboard', active: settingsPage === 'keyboard' },\n    { id: 'audio', label: 'Audio & Mic', active: settingsPage === 'audio' },\n    {\n      id: 'pricing-billing',\n      label: 'Pricing & Billing',\n      active: settingsPage === 'pricing-billing',\n    },\n    { id: 'account', label: 'Account', active: settingsPage === 'account' },\n    { id: 'advanced', label: 'Advanced', active: settingsPage === 'advanced' },\n  ]\n\n  const renderSettingsContent = () => {\n    switch (settingsPage) {\n      case 'general':\n        return <GeneralSettingsContent />\n      case 'keyboard':\n        return <KeyboardSettingsContent />\n      case 'audio':\n        return <AudioSettingsContent />\n      case 'pricing-billing':\n        return <PricingBillingSettingsContent />\n      case 'account':\n        return <AccountSettingsContent />\n      case 'advanced':\n        return <AdvancedSettingsContent />\n      default:\n        return <GeneralSettingsContent />\n    }\n  }\n\n  return (\n    <div className=\"w-full px-32\">\n      <div className=\"space-y-6\">\n        {/* Horizontal Tab/Pill Selector */}\n        <div className=\"flex gap-1 p-1 bg-slate-100 rounded-lg w-fit mx-auto\">\n          {settingsMenuItems.map(item => (\n            <button\n              key={item.id}\n              onClick={() => setSettingsPage(item.id as any)}\n              className={`px-4 py-2 rounded-md text-sm font-medium transition-all duration-200 ${\n                item.active\n                  ? 'bg-white text-slate-900 shadow-sm'\n                  : 'text-slate-600 hover:text-slate-900 hover:bg-slate-50'\n              }`}\n            >\n              {item.label}\n            </button>\n          ))}\n        </div>\n\n        {/* Content Area */}\n        <div className=\"w-full pt-8\">{renderSettingsContent()}</div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/home/contents/activityMessages.ts",
    "content": "// Activity message categories and levels\nexport interface ActivityMessage {\n  text: string\n}\n\nexport interface ActivityMessageLevel {\n  messages: ActivityMessage[]\n}\n\nexport interface ActivityMessageCategory {\n  levels: ActivityMessageLevel[]\n}\n\n// Weekly Streak Messages\nexport const STREAK_MESSAGES: ActivityMessageCategory = {\n  levels: [\n    {\n      messages: [\n        { text: 'Momentum starts now 🚀' },\n        { text: \"You're doing it! ❤️\" },\n        { text: 'Your spark just lit my heart ✨' },\n        { text: \"Great start! I've got your back 💪\" },\n      ],\n    },\n    {\n      messages: [\n        { text: \"You're on a roll 🌀\" },\n        { text: 'Your rhythm makes me grin 😁' },\n        { text: \"Keep going, we're in this! 🙌\" },\n        { text: 'Streak climbing 📈' },\n        { text: 'Love the consistency 💕' },\n      ],\n    },\n    {\n      messages: [\n        { text: 'A month strong! 💪' },\n        { text: 'Your streak inspires me daily 🌟' },\n        { text: 'Dedication looks good on you 😎' },\n        { text: 'Dedication unlocked 🔓' },\n        { text: \"We're building greatness together 🧱\" },\n      ],\n    },\n    {\n      messages: [\n        { text: '🔥🔥🔥🔥🔥' },\n        { text: 'Persistence icon 👑' },\n        { text: \"You're unstoppable, I feel it! 💥\" },\n        { text: 'Elite status earned 🌟' },\n        { text: \"Let's keep this magic alive ✨\" },\n      ],\n    },\n  ],\n}\n\n// Average Speed Messages\nexport const SPEED_MESSAGES: ActivityMessageCategory = {\n  levels: [\n    {\n      messages: [\n        { text: 'Warm-up complete 🔥' },\n        { text: \"Take your time, I'm listening 🧏\" },\n        { text: 'Starting steady 🎯' },\n        { text: 'Great pace! Keep going!' },\n      ],\n    },\n    {\n      messages: [\n        { text: \"Nice pace! I'm smiling big 😁\" },\n        { text: 'Flowing like friends chatting 🗣️' },\n        { text: 'Love this tempo, keep riffing 🎸' },\n        { text: 'You talk, I dance along 💃' },\n        { text: 'Our sync feels awesome 🎧' },\n      ],\n    },\n    {\n      messages: [\n        { text: \"Now we're talking!\" },\n        { text: 'Flow state achieved!' },\n        { text: \"You're on fire, I'm hype 🔥\" },\n        { text: 'Smooth operator! 💃' },\n        { text: 'Your flow fuels me 🚀' },\n      ],\n    },\n    {\n      messages: [\n        { text: \"Lightning! I'm awed 🤯\" },\n        { text: 'Top 1% - I knew you could! 🌟' },\n        { text: \"World can't match your pace 😎\" },\n        { text: 'Speed demon! 💥' },\n        { text: 'I race to keep up! 😂' },\n      ],\n    },\n  ],\n}\n\n// Total Words Messages\nexport const TOTAL_WORDS_MESSAGES: ActivityMessageCategory = {\n  levels: [\n    {\n      messages: [\n        { text: 'Every word counts!' },\n        { text: \"Seed planted, I'm excited 🌱\" },\n        { text: 'Great beginning!' },\n        { text: \"Story begins: I'm hooked 📖\" },\n        { text: 'Love hearing every word 🥰' },\n      ],\n    },\n    {\n      messages: [\n        { text: 'Thousands in! Proud partner 🙌' },\n        { text: \"Now that's a short story!\" },\n        { text: \"Paragraph party and I'm invited 🥳\" },\n        { text: 'Ideas streaming 🌊' },\n        { text: 'Nice momentum 🚀' },\n      ],\n    },\n    {\n      messages: [\n        { text: 'Dictation natural!' },\n        { text: 'Prolific vibes, my friend 🎶' },\n        { text: 'Word mountain rising ⛰️' },\n        { text: 'Author mode on 📝' },\n        { text: 'Consistency royalty 👑' },\n      ],\n    },\n    {\n      messages: [\n        { text: 'Library worth of words! 📚' },\n        { text: 'Status: Living legend 🔥' },\n        { text: 'Wordsmith wizardry 🪄' },\n        { text: 'You dictate history, buddy 🏛️' },\n        { text: \"My pride can't fit the page 😍\" },\n      ],\n    },\n  ],\n}\n\nexport const getStreakLevel = (streakDays: number): number => {\n  if (streakDays < 7) return 0\n  if (streakDays < 21) return 1\n  if (streakDays < 56) return 2\n  return 3\n}\n\nexport const getSpeedLevel = (averageWPM: number): number => {\n  if (averageWPM <= 100) return 0\n  if (averageWPM <= 200) return 1\n  if (averageWPM <= 300) return 2\n  return 3\n}\n\nexport const getTotalWordsLevel = (totalWords: number): number => {\n  if (totalWords <= 1000) return 0\n  if (totalWords <= 5000) return 1\n  if (totalWords <= 25000) return 2\n  return 3\n}\n\nexport const getActivityMessage = (\n  category: ActivityMessageCategory,\n  level: number,\n): string => {\n  const messages = category.levels[level]?.messages || []\n  if (messages.length === 0) return 'You are off to great start'\n\n  const hour = new Date().getHours()\n  const seed = hour % messages.length\n  return messages[seed].text\n}\n"
  },
  {
    "path": "app/components/home/contents/settings/AccountSettingsContent.tsx",
    "content": "import React, { useState } from 'react'\nimport { useNotesStore } from '../../../../store/useNotesStore'\nimport { useDictionaryStore } from '../../../../store/useDictionaryStore'\nimport { useOnboardingStore } from '../../../../store/useOnboardingStore'\nimport { Button } from '../../../ui/button'\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from '../../../ui/dialog'\nimport { useAuthStore } from '@/app/store/useAuthStore'\nimport { useAuth } from '@/app/components/auth/useAuth'\n\nexport default function AccountSettingsContent() {\n  const { user, setName, clearAuth } = useAuthStore()\n  const { logoutUser } = useAuth()\n  const { loadNotes } = useNotesStore()\n  const { loadEntries } = useDictionaryStore()\n  const { resetOnboarding } = useOnboardingStore()\n\n  const [showDeleteDialog, setShowDeleteDialog] = useState(false)\n\n  const handleSignOut = async () => {\n    try {\n      await logoutUser()\n    } catch (error) {\n      console.error('Logout failed:', error)\n    }\n  }\n\n  const handleDeleteAccount = async () => {\n    try {\n      // Delete user data from both local and server databases\n      // Server now extracts userId from authenticated user's token\n      await window.api.deleteUserData()\n\n      // Clear KV-backed app state\n      window.electron.store.set('settings', {})\n      window.electron.store.set('main', {})\n      window.electron.store.set('onboarding', {})\n      window.electron.store.set('auth', {})\n\n      // Clear auth state\n      clearAuth(false)\n\n      // Reset all stores to their initial state\n      resetOnboarding()\n      loadNotes()\n      loadEntries()\n\n      // Close the dialog\n      setShowDeleteDialog(false)\n\n      // Note: The app will automatically navigate to onboarding since user is no longer authenticated\n    } catch (error) {\n      console.error('Failed to delete account data:', error)\n      // Still proceed with local cleanup even if server deletion fails\n      // Clear KV-backed app state\n      window.electron.store.set('settings', {})\n      window.electron.store.set('main', {})\n      window.electron.store.set('onboarding', {})\n      window.electron.store.set('auth', {})\n\n      // Clear auth state\n      clearAuth(false)\n\n      // Reset all stores to their initial state\n      resetOnboarding()\n      loadNotes()\n      loadEntries()\n\n      // Close the dialog\n      setShowDeleteDialog(false)\n    }\n  }\n\n  return (\n    <div className=\"h-full justify-between\">\n      <div className=\"space-y-6\">\n        {/* First name */}\n        <div className=\"flex items-center justify-between\">\n          <label className=\"text-sm font-medium text-gray-900\">Name</label>\n          <input\n            type=\"text\"\n            value={user?.name}\n            onChange={e => setName(e.target.value)}\n            className=\"w-80 bg-white border border-gray-300 rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n          />\n        </div>\n\n        {/* Email */}\n        <div className=\"flex items-center justify-between py-3 my-1\">\n          <label className=\"text-sm font-medium text-gray-900\">Email</label>\n          <div className=\"w-80 text-sm text-gray-600 px-4\">{user?.email}</div>\n        </div>\n      </div>\n\n      {/* Action buttons */}\n      <div className=\"flex pt-8 w-full justify-center\">\n        <Button\n          variant=\"outline\"\n          size=\"lg\"\n          onClick={handleSignOut}\n          className=\"px-6 py-3 bg-neutral-200 text-neutral-700 hover:bg-neutral-300\"\n        >\n          Sign out\n        </Button>\n      </div>\n      <div className=\"flex pt-12 w-full justify-center\">\n        <Button\n          variant=\"ghost\"\n          size=\"lg\"\n          onClick={() => setShowDeleteDialog(true)}\n          className=\"px-6 py-3 text-red-400 hover:text-red-200\"\n        >\n          Delete account\n        </Button>\n      </div>\n\n      {/* Delete Confirmation Dialog */}\n      <Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>\n        <DialogContent className=\"sm:max-w-md\">\n          <DialogHeader>\n            <DialogTitle className=\"text-red-600\">Delete Account</DialogTitle>\n            <DialogDescription className=\"text-gray-600\">\n              Are you absolutely sure you want to delete your account? This\n              action cannot be undone and will permanently remove:\n              <br />\n              <br />\n              • All your personal information\n              <br />\n              • All saved notes\n              <br />\n              • All dictionary entries\n              <br />\n              • All app settings and preferences\n              <br />\n              <br />\n              This will reset Ito to its initial state.\n            </DialogDescription>\n          </DialogHeader>\n          <DialogFooter className=\"gap-3\">\n            <Button\n              variant=\"outline\"\n              onClick={() => setShowDeleteDialog(false)}\n            >\n              Cancel\n            </Button>\n            <Button variant=\"destructive\" onClick={handleDeleteAccount}>\n              Yes, delete everything\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/home/contents/settings/AdvancedSettingsContent.tsx",
    "content": "import {\n  LlmSettings,\n  useAdvancedSettingsStore,\n} from '@/app/store/useAdvancedSettingsStore'\nimport {\n  ChangeEvent,\n  useEffect,\n  useRef,\n  useState,\n  useCallback,\n  memo,\n} from 'react'\nimport { useWindowContext } from '@/app/components/window/WindowContext'\n\ntype LlmSettingConfig = {\n  name: keyof LlmSettings\n  label: string\n  placeholder: string\n  description: string\n  maxLength: number\n  resize?: boolean\n  readOnly?: boolean\n  isSelect?: boolean\n  options?: string[]\n}\n\nconst modelProviderLengthLimit = 30\nconst floatLengthLimit = 4\nconst asrPromptLengthLimit = 100\nconst llmPromptLengthLimit = 1500\n\nconst llmSettingsConfig: LlmSettingConfig[] = [\n  {\n    name: 'asrProvider',\n    label: 'ASR Provider',\n    placeholder: 'Enter ASR provider name',\n    description: '',\n    maxLength: modelProviderLengthLimit,\n    readOnly: true,\n  },\n  {\n    name: 'asrModel',\n    label: 'ASR Model',\n    placeholder: 'Enter ASR model name',\n    description: 'The ASR model used for speech-to-text transcription',\n    maxLength: modelProviderLengthLimit,\n  },\n  {\n    name: 'asrPrompt',\n    label: 'ASR Prompt',\n    placeholder: 'Enter custom ASR prompt',\n    description:\n      'A custom prompt to guide the ASR transcription process for better accuracy. Dictionary will be appended. (Leave empty for default)',\n    maxLength: asrPromptLengthLimit,\n    resize: true,\n  },\n  {\n    name: 'llmProvider',\n    label: 'LLM Provider',\n    placeholder: 'Select LLM provider',\n    description: 'LLM provider for text generation tasks',\n    maxLength: modelProviderLengthLimit,\n    isSelect: true,\n    options: ['groq', 'cerebras'],\n  },\n  {\n    name: 'llmModel',\n    label: 'LLM Model',\n    placeholder: 'Enter LLM model name',\n    description: 'The LLM model used for text generation tasks',\n    maxLength: modelProviderLengthLimit,\n  },\n  {\n    name: 'llmTemperature',\n    label: 'LLM Temperature',\n    placeholder: 'Enter LLM temperature (e.g., 0.7)',\n    description:\n      'Controls the randomness of the LLM output. Higher values produce more diverse results.',\n    maxLength: floatLengthLimit,\n  },\n  {\n    name: 'transcriptionPrompt',\n    label: 'Transcription Prompt',\n    placeholder: 'Enter custom transcription prompt',\n    description:\n      'A custom prompt to guide the transcription process for better accuracy. (Leave empty for default)',\n    maxLength: llmPromptLengthLimit,\n    resize: true,\n  },\n  // This is being removed until long term solution for versioning prompts is implemented\n  // https://github.com/heyito/ito/issues/174\n  // {\n  //   name: 'editingPrompt',\n  //   label: 'Editing Prompt',\n  //   placeholder: 'Enter custom editing prompt',\n  //   description:\n  //     'A custom prompt to guide the editing process for improved text quality. (Leave empty for default)',\n  //   maxLength: llmPromptLengthLimit,\n  //   resize: true,\n  // },\n  {\n    name: 'noSpeechThreshold',\n    label: 'No Speech Threshold',\n    placeholder: 'e.g., 0.6',\n    description: 'Threshold for detecting no speech segments in audio.',\n    maxLength: floatLengthLimit,\n  },\n]\n\nfunction formatDisplayValue(value: string | number | null): string {\n  if (value === null) {\n    return ''\n  }\n  // If its a number then format it to 2 decimal places\n  if (typeof value === 'number') {\n    return value.toFixed(2)\n  }\n  return value\n}\n\ninterface SettingInputProps {\n  config: LlmSettingConfig\n  value: string | number | null\n  onChange: (\n    e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,\n    config: LlmSettingConfig,\n  ) => void\n}\n\nconst SettingInput = memo(function SettingInput({\n  config,\n  value,\n  onChange,\n}: SettingInputProps) {\n  const [isFocused, setIsFocused] = useState(false)\n  const [editingValue, setEditingValue] = useState('')\n\n  const handleChange = useCallback(\n    (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {\n      const newValue = e.target.value\n      setEditingValue(newValue)\n      onChange(e, config)\n    },\n    [onChange, config],\n  )\n\n  const handleFocus = useCallback(() => {\n    setIsFocused(true)\n    // Start with the formatted display value to avoid jarring transition\n    const startValue = formatDisplayValue(value)\n    setEditingValue(startValue)\n  }, [value])\n\n  const handleBlur = useCallback(() => {\n    setIsFocused(false)\n    setEditingValue('')\n  }, [])\n\n  const displayValue = isFocused ? editingValue : formatDisplayValue(value)\n\n  return (\n    <div className=\"mb-5\">\n      <label\n        htmlFor={config.name}\n        className=\"block text-sm font-medium text-slate-700 mb-1 ml-1\"\n      >\n        {config.label}\n      </label>\n      {config.isSelect ? (\n        <select\n          id={config.name}\n          value={value ?? ''}\n          onChange={handleChange}\n          className=\"w-3/4 ml-1 px-3 py-2 border border-slate-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n          disabled={config.readOnly}\n        >\n          {config.options?.map(option => (\n            <option key={option} value={option}>\n              {option}\n            </option>\n          ))}\n        </select>\n      ) : (\n        <input\n          id={config.name}\n          value={displayValue}\n          onChange={handleChange}\n          onFocus={handleFocus}\n          onBlur={handleBlur}\n          className=\"w-3/4 ml-1 px-3 py-2 border border-slate-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n          placeholder={config.placeholder}\n          maxLength={config.maxLength}\n          readOnly={config.readOnly}\n        />\n      )}\n      <p className=\"w-3/4 text-xs text-slate-500 mt-1 ml-1\">\n        {config.description}\n      </p>\n    </div>\n  )\n})\n\nexport default function AdvancedSettingsContent() {\n  const {\n    llm,\n    defaults,\n    grammarServiceEnabled,\n    macosAccessibilityContextEnabled,\n    setLlmSettings,\n    setGrammarServiceEnabled,\n    setMacosAccessibilityContextEnabled,\n  } = useAdvancedSettingsStore()\n  const windowContext = useWindowContext()\n  const debounceRef = useRef<NodeJS.Timeout>(null)\n\n  // Helper to resolve null to actual default value for display\n  const getDisplayValue = useCallback(\n    (key: keyof LlmSettings): string | number | null => {\n      const value = llm[key]\n      if (value === null && defaults) {\n        return defaults[key] ?? null\n      }\n      return value\n    },\n    [llm, defaults],\n  )\n\n  useEffect(() => {\n    return () => {\n      if (debounceRef.current) {\n        clearTimeout(debounceRef.current)\n      }\n    }\n  }, [])\n\n  const scheduleAdvancedSettingsUpdate = useCallback(\n    (\n      nextLlm: LlmSettings,\n      nextGrammarEnabled: boolean,\n      nextMacosAccessibilityEnabled: boolean,\n    ) => {\n      if (debounceRef.current) {\n        clearTimeout(debounceRef.current)\n      }\n\n      debounceRef.current = setTimeout(async () => {\n        const settingsToSave = {\n          llm: nextLlm,\n          grammarServiceEnabled: nextGrammarEnabled,\n          macosAccessibilityContextEnabled: nextMacosAccessibilityEnabled,\n        }\n        await window.api.updateAdvancedSettings(settingsToSave)\n      }, 1000)\n    },\n    [],\n  )\n\n  const handleInputChange = useCallback(\n    (\n      e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,\n      config: LlmSettingConfig,\n    ) => {\n      const rawValue = e.target.value\n\n      // Determine if this field should be a number\n      const isNumericField =\n        config.name === 'llmTemperature' || config.name === 'noSpeechThreshold'\n\n      // Parse the value appropriately\n      let newValue: string | number | null\n      if (rawValue === '') {\n        newValue = null\n      } else if (isNumericField) {\n        const parsed = parseFloat(rawValue)\n        newValue = isNaN(parsed) ? null : parsed\n      } else {\n        newValue = rawValue\n      }\n\n      const updatedLlm = { ...llm, [config.name]: newValue }\n      setLlmSettings({ [config.name]: newValue })\n      scheduleAdvancedSettingsUpdate(\n        updatedLlm,\n        grammarServiceEnabled,\n        macosAccessibilityContextEnabled,\n      )\n    },\n    [\n      llm,\n      grammarServiceEnabled,\n      macosAccessibilityContextEnabled,\n      setLlmSettings,\n      scheduleAdvancedSettingsUpdate,\n    ],\n  )\n\n  const handleGrammarServiceToggle = useCallback(\n    (e: ChangeEvent<HTMLInputElement>) => {\n      const enabled = e.target.checked\n      setGrammarServiceEnabled(enabled)\n      scheduleAdvancedSettingsUpdate(\n        llm,\n        enabled,\n        macosAccessibilityContextEnabled,\n      )\n    },\n    [\n      llm,\n      macosAccessibilityContextEnabled,\n      setGrammarServiceEnabled,\n      scheduleAdvancedSettingsUpdate,\n    ],\n  )\n\n  const handleMacosAccessibilityContextToggle = useCallback(\n    (e: ChangeEvent<HTMLInputElement>) => {\n      const enabled = e.target.checked\n      setMacosAccessibilityContextEnabled(enabled)\n      scheduleAdvancedSettingsUpdate(llm, grammarServiceEnabled, enabled)\n    },\n    [\n      llm,\n      grammarServiceEnabled,\n      setMacosAccessibilityContextEnabled,\n      scheduleAdvancedSettingsUpdate,\n    ],\n  )\n\n  const handleRestoreDefaults = useCallback(() => {\n    const defaultLlmSettings: LlmSettings = {\n      asrProvider: null,\n      asrModel: null,\n      asrPrompt: null,\n      llmProvider: null,\n      llmModel: null,\n      llmTemperature: null,\n      transcriptionPrompt: null,\n      editingPrompt: null,\n      noSpeechThreshold: null,\n    }\n    setLlmSettings(defaultLlmSettings)\n    scheduleAdvancedSettingsUpdate(\n      defaultLlmSettings,\n      grammarServiceEnabled,\n      macosAccessibilityContextEnabled,\n    )\n  }, [\n    grammarServiceEnabled,\n    macosAccessibilityContextEnabled,\n    setLlmSettings,\n    scheduleAdvancedSettingsUpdate,\n  ])\n\n  return (\n    <div className=\"max-h-[70vh] overflow-y-auto scrollbar-thin scrollbar-thumb-slate-500 scrollbar-track-transparent\">\n      {/* LLM Settings Section */}\n      <div className=\"space-y-6\">\n        <div>\n          <div className=\"flex items-center justify-between mb-3 ml-1 mr-1\">\n            <h3 className=\"text-md font-medium text-slate-900\">LLM Settings</h3>\n            <button\n              onClick={handleRestoreDefaults}\n              className=\"px-3 py-1 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-md hover:bg-slate-50 transition-colors\"\n            >\n              Restore Defaults\n            </button>\n          </div>\n          <div className=\"space-y-3\">\n            {llmSettingsConfig.map(config => (\n              <SettingInput\n                key={config.name}\n                config={config}\n                value={getDisplayValue(config.name)}\n                onChange={handleInputChange}\n              />\n            ))}\n          </div>\n        </div>\n\n        <div>\n          <h3 className=\"text-md font-medium text-slate-900 mb-3 ml-1\">\n            Grammar\n          </h3>\n          <label className=\"flex items-start gap-3 ml-1\">\n            <input\n              type=\"checkbox\"\n              checked={grammarServiceEnabled}\n              onChange={handleGrammarServiceToggle}\n              className=\"mt-1 h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500\"\n            />\n            <span>\n              <span className=\"block text-sm font-medium text-slate-700\">\n                Enable Grammar Service\n              </span>\n              <span className=\"block text-xs text-slate-500 mt-1\">\n                Apply Ito's local grammar adjustments before inserting text.\n              </span>\n            </span>\n          </label>\n        </div>\n\n        {windowContext?.window?.platform === 'darwin' && (\n          <div>\n            <h3 className=\"text-md font-medium text-slate-900 mb-3 ml-1\">\n              Context\n            </h3>\n            <label className=\"flex items-start gap-3 ml-1\">\n              <input\n                type=\"checkbox\"\n                checked={macosAccessibilityContextEnabled}\n                onChange={handleMacosAccessibilityContextToggle}\n                className=\"mt-1 h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500\"\n              />\n              <span>\n                <span className=\"block text-sm font-medium text-slate-700\">\n                  Use Accessibility Context\n                </span>\n                <span className=\"block text-xs text-slate-500 mt-1\">\n                  Use Accessibility APIs to capture text context around the\n                  cursor for improved accuracy.\n                </span>\n              </span>\n            </label>\n          </div>\n        )}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/home/contents/settings/AudioSettingsContent.tsx",
    "content": "import { Switch } from '@/app/components/ui/switch'\nimport { MicrophoneSelector } from '@/app/components/ui/microphone-selector'\nimport { useSettingsStore } from '@/app/store/useSettingsStore'\n\nexport default function AudioSettingsContent() {\n  const {\n    microphoneDeviceId,\n    microphoneName,\n    // interactionSounds,\n    muteAudioWhenDictating,\n    setMicrophoneDeviceId,\n    // setInteractionSounds,\n    setMuteAudioWhenDictating,\n  } = useSettingsStore()\n\n  return (\n    <div className=\"space-y-8\">\n      <div>\n        <div className=\"space-y-6\">\n          {/* <div className=\"flex items-center justify-between\">\n            <div>\n              <div className=\"text-sm font-medium\">Interaction Sounds</div>\n              <div className=\"text-xs text-gray-600 mt-1\">\n                Play a sound when Ito starts and stops recording.\n              </div>\n            </div>\n            <Switch\n              checked={interactionSounds}\n              onCheckedChange={setInteractionSounds}\n            />\n          </div> */}\n\n          <div className=\"flex items-center justify-between\">\n            <div>\n              <div className=\"text-sm font-medium\">\n                Mute audio when dictating\n              </div>\n              <div className=\"text-xs text-gray-600 mt-1\">\n                Automatically silence other active audio during dictation.\n              </div>\n            </div>\n            <Switch\n              checked={muteAudioWhenDictating}\n              onCheckedChange={setMuteAudioWhenDictating}\n            />\n          </div>\n\n          <div className=\"flex justify-between\">\n            <div>\n              <div className=\"text-sm font-medium mb-2\">\n                Select default microphone\n              </div>\n              <div className=\"text-xs text-gray-600 mt-1\">\n                Select the microphone Ito will use by default for audio input.\n              </div>\n            </div>\n            <MicrophoneSelector\n              selectedDeviceId={microphoneDeviceId}\n              selectedMicrophoneName={microphoneName}\n              onSelectionChange={setMicrophoneDeviceId}\n              triggerButtonVariant=\"outline\"\n              triggerButtonClassName=\"\"\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/home/contents/settings/GeneralSettingsContent.tsx",
    "content": "import { useState } from 'react'\nimport { Switch } from '@/app/components/ui/switch'\nimport { Button } from '@/app/components/ui/button'\nimport { useSettingsStore } from '@/app/store/useSettingsStore'\nimport { useWindowContext } from '@/app/components/window/WindowContext'\n\nexport default function GeneralSettingsContent() {\n  const [isDownloading, setIsDownloading] = useState(false)\n  const [isClearing, setIsClearing] = useState(false)\n  const {\n    shareAnalytics,\n    launchAtLogin,\n    showItoBarAlways,\n    showAppInDock,\n    setShareAnalytics,\n    setLaunchAtLogin,\n    setShowItoBarAlways,\n    setShowAppInDock,\n  } = useSettingsStore()\n\n  const windowContext = useWindowContext()\n\n  const handleDownloadLogs = async () => {\n    setIsDownloading(true)\n    try {\n      const result = await window.api.logs.download()\n      if (result.success) {\n        console.log('Logs downloaded successfully to:', result.path)\n      } else {\n        if (result.error !== 'Download cancelled') {\n          console.error('Failed to download logs:', result.error)\n          alert(`Failed to download logs: ${result.error}`)\n        }\n      }\n    } catch (error) {\n      console.error('Error downloading logs:', error)\n      alert('An unexpected error occurred while downloading logs')\n    } finally {\n      setIsDownloading(false)\n    }\n  }\n\n  const handleClearLogs = async () => {\n    const confirmed = confirm(\n      'Are you sure you want to clear all logs? This action cannot be undone.',\n    )\n    if (!confirmed) return\n\n    setIsClearing(true)\n    try {\n      const result = await window.api.logs.clear()\n      if (result.success) {\n        console.log('Logs cleared successfully')\n        alert('Logs cleared successfully')\n      } else {\n        console.error('Failed to clear logs:', result.error)\n        alert(`Failed to clear logs: ${result.error}`)\n      }\n    } catch (error) {\n      console.error('Error clearing logs:', error)\n      alert('An unexpected error occurred while clearing logs')\n    } finally {\n      setIsClearing(false)\n    }\n  }\n\n  return (\n    <div className=\"space-y-8\">\n      <div>\n        <div className=\"space-y-6\">\n          <div className=\"flex items-center justify-between\">\n            <div>\n              <div className=\"text-sm font-medium\">Share analytics</div>\n              <div className=\"text-xs text-gray-600 mt-1\">\n                Share anonymous usage data to help us improve Ito.\n              </div>\n            </div>\n            <Switch\n              checked={shareAnalytics}\n              onCheckedChange={setShareAnalytics}\n            />\n          </div>\n\n          <div className=\"flex items-center justify-between\">\n            <div>\n              <div className=\"text-sm font-medium\">Launch at Login</div>\n              <div className=\"text-xs text-gray-600 mt-1\">\n                Open Ito automatically when your computer starts.\n              </div>\n            </div>\n            <Switch\n              checked={launchAtLogin}\n              onCheckedChange={setLaunchAtLogin}\n            />\n          </div>\n\n          <div className=\"flex items-center justify-between\">\n            <div>\n              <div className=\"text-sm font-medium\">\n                Show Ito bar at all times\n              </div>\n              <div className=\"text-xs text-gray-600 mt-1\">\n                Show the Ito bar at all times.\n              </div>\n            </div>\n            <Switch\n              checked={showItoBarAlways}\n              onCheckedChange={setShowItoBarAlways}\n            />\n          </div>\n\n          {windowContext?.window?.platform === 'darwin' && (\n            <div className=\"flex items-center justify-between\">\n              <div>\n                <div className=\"text-sm font-medium\">Show app in dock</div>\n                <div className=\"text-xs text-gray-600 mt-1\">\n                  Show the Ito app in the dock for quick access.\n                </div>\n              </div>\n              <Switch\n                checked={showAppInDock}\n                onCheckedChange={setShowAppInDock}\n              />\n            </div>\n          )}\n        </div>\n      </div>\n\n      <div>\n        <div className=\"text-lg font-medium mb-4\">Log Management</div>\n        <div className=\"space-y-3\">\n          <div className=\"flex items-center justify-between\">\n            <div>\n              <div className=\"text-sm font-medium\">Download Logs</div>\n              <div className=\"text-xs text-gray-600 mt-1\">\n                Export your local logs to a file for troubleshooting.\n              </div>\n            </div>\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              onClick={handleDownloadLogs}\n              disabled={isDownloading}\n            >\n              {isDownloading ? 'Downloading...' : 'Download'}\n            </Button>\n          </div>\n\n          <div className=\"flex items-center justify-between\">\n            <div>\n              <div className=\"text-sm font-medium\">Clear Logs</div>\n              <div className=\"text-xs text-gray-600 mt-1\">\n                Permanently delete all local logs from your device.\n              </div>\n            </div>\n            <Button\n              variant=\"destructive\"\n              size=\"sm\"\n              onClick={handleClearLogs}\n              disabled={isClearing}\n            >\n              {isClearing ? 'Clearing...' : 'Clear'}\n            </Button>\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/home/contents/settings/KeyboardSettingsContent.tsx",
    "content": "import { useSettingsStore } from '@/app/store/useSettingsStore'\nimport { ItoMode } from '@/app/generated/ito_pb'\nimport MultiShortcutEditor from '@/app/components/ui/multi-shortcut-editor'\n\nexport default function KeyboardSettingsContent() {\n  const { getItoModeShortcuts } = useSettingsStore()\n  const transcribeShortcuts = getItoModeShortcuts(ItoMode.TRANSCRIBE)\n  const editShortcuts = getItoModeShortcuts(ItoMode.EDIT)\n\n  return (\n    <div className=\"space-y-8\">\n      <div>\n        <div className=\"space-y-6\">\n          <div className=\"flex gap-4 justify-between\">\n            <div className=\"w-1/3\">\n              <div className=\"text-sm font-medium mb-2\">Keyboard Shortcut</div>\n              <div className=\"text-xs text-gray-600 mb-4\">\n                Set the keyboard shortcut to activate Ito. Press the keys you\n                want to use for your shortcut.\n              </div>\n            </div>\n            <MultiShortcutEditor\n              shortcuts={transcribeShortcuts}\n              mode={ItoMode.TRANSCRIBE}\n            />\n          </div>\n          <div className=\"flex gap-4 justify-between\">\n            <div className=\"w-1/3\">\n              <div className=\"text-sm font-medium mb-2\">\n                Intelligent Mode Shortcut\n              </div>\n              <div className=\"text-xs text-gray-600 mb-4\">\n                Set the shortcut to activate Intelligent Mode. Press your\n                hotkey, speak to Ito, and the LLM's output is pasted into your\n                text box.\n              </div>\n            </div>\n            <MultiShortcutEditor\n              shortcuts={editShortcuts}\n              mode={ItoMode.EDIT}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/home/contents/settings/PricingBillingSettingsContent.tsx",
    "content": "import { useState, useEffect } from 'react'\nimport { Button } from '@/app/components/ui/button'\nimport { Check } from '@mynaui/icons-react'\nimport useBillingState from '@/app/hooks/useBillingState'\n\ntype BillingPeriod = 'monthly' | 'annual'\n\nexport default function PricingBillingSettingsContent() {\n  const [billingPeriod, setBillingPeriod] = useState<BillingPeriod>('annual')\n  const billingState = useBillingState()\n  const [checkoutLoading, setCheckoutLoading] = useState(false)\n  const [checkoutError, setCheckoutError] = useState<string | null>(null)\n  const [downgradeLoading, setDowngradeLoading] = useState(false)\n  const [reactivateLoading, setReactivateLoading] = useState(false)\n\n  // Refresh billing state when checkout session completes\n  useEffect(() => {\n    const offSuccess = window.api.on('billing-session-completed', async () => {\n      // Refresh billing state to reflect the new subscription\n      await billingState.refresh()\n      setCheckoutError(null)\n    })\n\n    return () => {\n      offSuccess?.()\n    }\n  }, [billingState])\n\n  const handleCheckout = async () => {\n    setCheckoutLoading(true)\n    setCheckoutError(null)\n    try {\n      const res = await window.api.billing.createCheckoutSession()\n      if (res?.success && res?.url) {\n        await window.api.invoke('web-open-url', res.url)\n      } else {\n        setCheckoutError(\n          res?.error || 'Failed to create checkout session. Please try again.',\n        )\n      }\n    } catch (err: any) {\n      setCheckoutError(\n        err?.message || 'Failed to create checkout session. Please try again.',\n      )\n    } finally {\n      setCheckoutLoading(false)\n    }\n  }\n\n  const handleDowngrade = async () => {\n    setDowngradeLoading(true)\n    setCheckoutError(null)\n    try {\n      const res = await window.api.billing.cancelSubscription()\n      if (res?.success) {\n        await billingState.refresh()\n      } else {\n        setCheckoutError(\n          res?.error || 'Failed to cancel subscription. Please try again.',\n        )\n      }\n    } catch (err: any) {\n      setCheckoutError(\n        err?.message || 'Failed to cancel subscription. Please try again.',\n      )\n    } finally {\n      setDowngradeLoading(false)\n    }\n  }\n\n  const handleReactivate = async () => {\n    setReactivateLoading(true)\n    setCheckoutError(null)\n    try {\n      const res = await window.api.billing.reactivateSubscription()\n      if (res?.success) {\n        await billingState.refresh()\n      } else {\n        setCheckoutError(\n          res?.error || 'Failed to reactivate subscription. Please try again.',\n        )\n      }\n    } catch (err: any) {\n      setCheckoutError(\n        err?.message || 'Failed to reactivate subscription. Please try again.',\n      )\n    } finally {\n      setReactivateLoading(false)\n    }\n  }\n\n  const handleContactUs = () => {\n    window.api.openMailto('support@ito.ai')\n  }\n\n  // Determine button states based on billing status\n  const getStarterButtonText = () => {\n    if (billingState.isLoading) return 'Loading...'\n    if (downgradeLoading) return 'Cancelling...'\n    if (billingState.isScheduledForCancellation) return 'Current Plan'\n    if (billingState.proStatus === 'active_pro') return 'Downgrade plan'\n    if (billingState.proStatus === 'free_trial') return 'Downgrade plan'\n    if (billingState.proStatus === 'none' && !billingState.isTrialActive) {\n      return 'Current plan'\n    }\n    return 'Current plan'\n  }\n\n  const getStarterButtonDisabled = () => {\n    return (\n      (billingState.proStatus === 'none' && !billingState.isTrialActive) ||\n      billingState.isLoading ||\n      downgradeLoading ||\n      billingState.isScheduledForCancellation\n    )\n  }\n\n  const getProButtonText = () => {\n    if (checkoutLoading) return 'Loading...'\n    if (billingState.isLoading) return 'Loading...'\n    if (reactivateLoading) return 'Reactivating...'\n    if (billingState.isScheduledForCancellation) return 'Reactivate'\n    if (billingState.proStatus === 'active_pro') return 'Current plan'\n    if (billingState.proStatus === 'free_trial') return 'Upgrade Plan'\n    return 'Upgrade'\n  }\n\n  const getProButtonDisabled = () => {\n    return (\n      (billingState.proStatus === 'active_pro' &&\n        !billingState.isScheduledForCancellation) ||\n      billingState.isLoading ||\n      checkoutLoading ||\n      reactivateLoading\n    )\n  }\n\n  const getProCardTitle = () => {\n    if (billingState.isTrialActive && billingState.daysLeft > 0) {\n      const dayText = billingState.daysLeft === 1 ? 'day' : 'days'\n      return `Pro Trial (${billingState.daysLeft} ${dayText} remaining)`\n    }\n    return 'Pro'\n  }\n\n  return (\n    <div className=\"space-y-8\">\n      {/* TODO: Integrate later  */}\n      {/* Billing Period Toggle */}\n      {/* <div className=\"flex items-center justify-center gap-3\">\n        <span\n          className={`text-sm font-medium ${billingPeriod === 'monthly' ? 'text-gray-900' : 'text-gray-500'}`}\n        >\n          Monthly\n        </span>\n        <Switch\n          checked={billingPeriod === 'annual'}\n          onCheckedChange={checked =>\n            setBillingPeriod(checked ? 'annual' : 'monthly')\n          }\n        />\n        <span\n          className={`text-sm font-medium ${billingPeriod === 'annual' ? 'text-gray-900' : 'text-gray-500'}`}\n        >\n          Annual\n        </span>\n        <span className=\"text-sm text-green-600 font-medium\">Saved 20%</span>\n      </div> */}\n\n      {/* Error Message */}\n      {checkoutError && (\n        <div className=\"bg-red-50 border border-red-200 rounded-lg p-4 text-sm text-red-800\">\n          {checkoutError}\n        </div>\n      )}\n\n      {/* Cancellation Notice */}\n      {billingState.isScheduledForCancellation &&\n        billingState.subscriptionEndAt && (\n          <div className=\"bg-yellow-50 border border-yellow-200 rounded-lg p-4 text-sm text-yellow-800\">\n            <p className=\"font-medium mb-1\">\n              Your subscription will end on{' '}\n              {billingState.subscriptionEndAt.toLocaleDateString('en-US', {\n                year: 'numeric',\n                month: 'long',\n                day: 'numeric',\n              })}\n            </p>\n            <p className=\"text-yellow-700\">\n              You'll continue to have Pro access until then. You can reactivate\n              anytime before the end date.\n            </p>\n          </div>\n        )}\n\n      {/* Pricing Cards */}\n      <div className=\"grid grid-cols-3 gap-6\">\n        {/* Starter Card */}\n        <PricingCard\n          title=\"Starter\"\n          price=\"FREE\"\n          features={[\n            '4,000 words per week',\n            'Lightning fast voice-typing',\n            'Add words to dictionary',\n            'Support for 100+ languages',\n          ]}\n          actionButton={\n            <Button\n              variant=\"outline\"\n              size=\"lg\"\n              className=\"w-full rounded-xl\"\n              disabled={getStarterButtonDisabled()}\n              onClick={handleDowngrade}\n            >\n              {getStarterButtonText()}\n            </Button>\n          }\n        />\n\n        {/* Pro Card */}\n        <PricingCard\n          title={getProCardTitle()}\n          price=\"$8.99\"\n          priceSubtext=\"/ month\"\n          isHighlighted\n          features={[\n            'Everything in Starter, and',\n            'Unlimited words per week',\n            'Ultra fast dictation as fast as 0.3 second',\n            'Priority customer support',\n            'Early access to new functionality',\n          ]}\n          actionButton={\n            <Button\n              variant=\"default\"\n              size=\"lg\"\n              className=\"w-full bg-gray-900 hover:bg-gray-800 text-white rounded-xl\"\n              disabled={getProButtonDisabled()}\n              onClick={\n                billingState.isScheduledForCancellation\n                  ? handleReactivate\n                  : handleCheckout\n              }\n            >\n              {getProButtonText()}\n            </Button>\n          }\n        />\n\n        {/* Team/Enterprise Card */}\n        <PricingCard\n          title=\"Team\"\n          price=\"Enterprise\"\n          features={[\n            'Admin controls',\n            'Shared resources',\n            'Shared dictionary',\n            'SOC 2 compliance',\n          ]}\n          actionButton={\n            <Button\n              variant=\"outline\"\n              size=\"lg\"\n              className=\"w-full rounded-xl border-gray-200\"\n              onClick={handleContactUs}\n            >\n              Contact Us\n            </Button>\n          }\n        />\n      </div>\n    </div>\n  )\n}\n\ninterface PricingCardProps {\n  title: string\n  price: string\n  priceSubtext?: string\n  features: string[]\n  actionButton: React.ReactNode\n  isHighlighted?: boolean\n}\n\nfunction PricingCard({\n  title,\n  price,\n  priceSubtext,\n  features,\n  actionButton,\n  isHighlighted = false,\n}: PricingCardProps) {\n  return (\n    <div\n      className={`rounded-xl border-2 p-6 flex flex-col ${\n        isHighlighted\n          ? 'border-purple-500 bg-gradient-to-br from-purple-50/30 to-pink-50/30'\n          : 'border-gray-200 bg-white'\n      }`}\n    >\n      {/* Title */}\n      <div className=\"text-sm font-medium text-gray-700 mb-2\">{title}</div>\n\n      {/* Price */}\n      <div className=\"mb-6\">\n        <span\n          className={`text-4xl font-bold ${\n            isHighlighted\n              ? 'bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent'\n              : 'text-gray-900'\n          }`}\n        >\n          {price}\n        </span>\n        {priceSubtext && (\n          <span className=\"text-gray-600 ml-1\">{priceSubtext}</span>\n        )}\n      </div>\n\n      {/* Features */}\n      <div className=\"flex-1 space-y-3 mb-6\">\n        {features.map((feature, index) => (\n          <div key={index} className=\"flex items-start gap-3\">\n            <div className=\"flex-shrink-0 mt-0.5\">\n              <Check className=\"w-5 h-5\" strokeWidth={3} />\n            </div>\n            <span className=\"text-sm text-gray-900\">{feature}</span>\n          </div>\n        ))}\n      </div>\n\n      {/* Action Button */}\n      <div className=\"mt-auto\">{actionButton}</div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/icons/AppleIcon.tsx",
    "content": "import React from 'react'\n\ninterface AppleIconProps {\n  width?: number\n  height?: number\n  className?: string\n}\n\nexport default function AppleIcon({\n  width = 16,\n  height = 16,\n  className,\n}: AppleIconProps) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 50 50\"\n      width={width}\n      height={height}\n      className={className}\n      fill=\"currentColor\"\n    >\n      <path d=\"M 44.527344 34.75 C 43.449219 37.144531 42.929688 38.214844 41.542969 40.328125 C 39.601563 43.28125 36.863281 46.96875 33.480469 46.992188 C 30.46875 47.019531 29.691406 45.027344 25.601563 45.0625 C 21.515625 45.082031 20.664063 47.03125 17.648438 47 C 14.261719 46.96875 11.671875 43.648438 9.730469 40.699219 C 4.300781 32.429688 3.726563 22.734375 7.082031 17.578125 C 9.457031 13.921875 13.210938 11.773438 16.738281 11.773438 C 20.332031 11.773438 22.589844 13.746094 25.558594 13.746094 C 28.441406 13.746094 30.195313 11.769531 34.351563 11.769531 C 37.492188 11.769531 40.8125 13.480469 43.1875 16.433594 C 35.421875 20.691406 36.683594 31.78125 44.527344 34.75 Z M 31.195313 8.46875 C 32.707031 6.527344 33.855469 3.789063 33.4375 1 C 30.972656 1.167969 28.089844 2.742188 26.40625 4.78125 C 24.878906 6.640625 23.613281 9.398438 24.105469 12.066406 C 26.796875 12.152344 29.582031 10.546875 31.195313 8.46875 Z\" />\n    </svg>\n  )\n}\n"
  },
  {
    "path": "app/components/icons/AppleNotesIcon.tsx",
    "content": "import React from 'react'\n\ninterface AppleNotesIconProps extends React.SVGProps<SVGSVGElement> {\n  width?: number\n  height?: number\n  className?: string\n  style?: React.CSSProperties\n}\n\nexport default function AppleNotesIcon({\n  width = 24,\n  height = 24,\n  className,\n  style,\n  ...props\n}: AppleNotesIconProps) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={width}\n      height={height}\n      viewBox=\"0 0 120 120\"\n      className={className}\n      style={style}\n      fill=\"none\"\n      {...props}\n    >\n      <defs>\n        <linearGradient\n          id=\"apple-notes-gradient\"\n          x1=\"50%\"\n          x2=\"50%\"\n          y1=\"0%\"\n          y2=\"100%\"\n        >\n          <stop offset=\"0%\" stopColor=\"#F4D87E\" />\n          <stop offset=\"100%\" stopColor=\"#F5C52C\" />\n        </linearGradient>\n        <filter\n          id=\"apple-notes-shadow\"\n          width=\"110.2%\"\n          height=\"146.7%\"\n          x=\"-5.1%\"\n          y=\"-16.7%\"\n          filterUnits=\"objectBoundingBox\"\n        >\n          <feOffset dy=\"2\" in=\"SourceAlpha\" result=\"shadowOffsetOuter1\" />\n          <feGaussianBlur\n            in=\"shadowOffsetOuter1\"\n            result=\"shadowBlurOuter1\"\n            stdDeviation=\"2\"\n          />\n          <feColorMatrix\n            in=\"shadowBlurOuter1\"\n            values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0\"\n          />\n        </filter>\n      </defs>\n      <rect width=\"120\" height=\"120\" rx=\"28\" fill=\"#FFF\" />\n      <g>\n        <g>\n          <g>\n            <g filter=\"url(#apple-notes-shadow)\">\n              <rect\n                x=\"-9\"\n                y=\"0\"\n                width=\"137\"\n                height=\"30\"\n                fill=\"url(#apple-notes-gradient)\"\n              />\n            </g>\n            <rect\n              x=\"-9\"\n              y=\"0\"\n              width=\"137\"\n              height=\"30\"\n              fill=\"url(#apple-notes-gradient)\"\n            />\n          </g>\n        </g>\n      </g>\n      <rect y=\"59\" width=\"120\" height=\"2\" fill=\"#C7C5C9\" />\n      <rect y=\"89\" width=\"120\" height=\"2\" fill=\"#C7C5C9\" />\n      <g fill=\"#C2C0C4\">\n        {[...Array(24)].map((_, i) => (\n          <circle key={i} cx={1.5 + 5 * i} cy={36.5} r={1.5} />\n        ))}\n      </g>\n    </svg>\n  )\n}\n"
  },
  {
    "path": "app/components/icons/AsterikIcon.tsx",
    "content": "const AsterikIcon = props => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    data-name=\"Layer 1\"\n    viewBox=\"0 0 24 24\"\n    {...props}\n  >\n    <path d=\"m20.537 12.7-1.13-.7 1.131-.7a4.126 4.126 0 0 0 1.729-2.031 3.919 3.919 0 0 0-3.28-5.272 4.124 4.124 0 0 0-2.586.654L16 4.9v-.728A4.116 4.116 0 0 0 12.393.019 4 4 0 0 0 8 4v.9l-.4-.25a4.122 4.122 0 0 0-2.587-.657 3.918 3.918 0 0 0-3.283 5.27 4.123 4.123 0 0 0 1.73 2.031L4.593 12l-1.131.7a4.126 4.126 0 0 0-1.729 2.031 3.918 3.918 0 0 0 3.286 5.272 4.124 4.124 0 0 0 2.581-.651L8 19.1v.9a4 4 0 0 0 8 0v-.9l.4.251a4.126 4.126 0 0 0 2.58.653 3.918 3.918 0 0 0 3.284-5.272 4.128 4.128 0 0 0-1.727-2.032zm-.311 4.418a1.916 1.916 0 0 1-2.639.613l-2.059-1.282A1 1 0 0 0 14 17.3V20a2 2 0 0 1-4 0v-2.7a1 1 0 0 0-1.528-.849l-2.059 1.284a1.915 1.915 0 1 1-2.025-3.252l2.625-1.634a1 1 0 0 0 0-1.7L4.388 9.516a1.915 1.915 0 0 1 2.025-3.252l2.059 1.282A1 1 0 0 0 10 6.7V4.107a2.075 2.075 0 0 1 1.664-2.08A2 2 0 0 1 14 4v2.7a1 1 0 0 0 1.528.848l2.059-1.281a1.915 1.915 0 1 1 2.025 3.252l-2.625 1.634a1 1 0 0 0 0 1.7l2.625 1.634a1.914 1.914 0 0 1 .614 2.638z\" />\n  </svg>\n)\nexport default AsterikIcon\n"
  },
  {
    "path": "app/components/icons/AudioIcon.tsx",
    "content": "export const AudioIcon = () => {\n  return (\n    <svg\n      width=\"24\"\n      height=\"16\"\n      viewBox=\"0 0 24 16\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <rect x=\"2\" y=\"6\" width=\"1.5\" height=\"4\" rx=\"1\" fill=\"#9CA3AF\" />\n      <rect x=\"5\" y=\"3\" width=\"1.5\" height=\"10\" rx=\"1\" fill=\"#9CA3AF\" />\n      <rect x=\"8\" y=\"0\" width=\"1.5\" height=\"16\" rx=\"1\" fill=\"#9CA3AF\" />\n      <rect x=\"11\" y=\"3\" width=\"1.5\" height=\"10\" rx=\"1\" fill=\"#9CA3AF\" />\n      <rect x=\"14\" y=\"6\" width=\"1.5\" height=\"4\" rx=\"1\" fill=\"#9CA3AF\" />\n    </svg>\n  )\n}\n"
  },
  {
    "path": "app/components/icons/AvatarIcon.tsx",
    "content": "const AvatarIcon = (props: any) => (\n  <svg\n    viewBox=\"0 0 128 128\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={260}\n    height={260}\n    {...props}\n  >\n    <circle cx=\"64\" cy=\"64\" r=\"64\" fill=\"#c4b5fd\" />\n    <circle cx=\"64\" cy=\"50\" r=\"20\" fill=\"#f3e8ff\" />\n    <ellipse cx=\"64\" cy=\"92\" rx=\"38\" ry=\"16\" fill=\"#a78bfa\" />\n  </svg>\n)\n\nexport default AvatarIcon\n"
  },
  {
    "path": "app/components/icons/ChatGPTIcon.tsx",
    "content": "interface ChatGPTIconProps {\n  className?: string\n}\n\nconst ChatGPTIcon = ({ className }: ChatGPTIconProps) => (\n  <svg\n    fill=\"currentColor\"\n    fillRule=\"evenodd\"\n    height=\"100%\"\n    style={{ flex: 'none', lineHeight: '1' }}\n    viewBox=\"0 0 24 24\"\n    width=\"100%\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    className={className}\n  >\n    <title>OpenAI</title>\n    <path d=\"M21.55 10.004a5.416 5.416 0 00-.478-4.501c-1.217-2.09-3.662-3.166-6.05-2.66A5.59 5.59 0 0010.831 1C8.39.995 6.224 2.546 5.473 4.838A5.553 5.553 0 001.76 7.496a5.487 5.487 0 00.691 6.5 5.416 5.416 0 00.477 4.502c1.217 2.09 3.662 3.165 6.05 2.66A5.586 5.586 0 0013.168 23c2.443.006 4.61-1.546 5.361-3.84a5.553 5.553 0 003.715-2.66 5.488 5.488 0 00-.693-6.497v.001zm-8.381 11.558a4.199 4.199 0 01-2.675-.954c.034-.018.093-.05.132-.074l4.44-2.53a.71.71 0 00.364-.623v-6.176l1.877 1.069c.02.01.033.029.036.05v5.115c-.003 2.274-1.87 4.118-4.174 4.123zM4.192 17.78a4.059 4.059 0 01-.498-2.763c.032.02.09.055.131.078l4.44 2.53c.225.13.504.13.73 0l5.42-3.088v2.138a.068.068 0 01-.027.057L9.9 19.288c-1.999 1.136-4.552.46-5.707-1.51h-.001zM3.023 8.216A4.15 4.15 0 015.198 6.41l-.002.151v5.06a.711.711 0 00.364.624l5.42 3.087-1.876 1.07a.067.067 0 01-.063.005l-4.489-2.559c-1.995-1.14-2.679-3.658-1.53-5.63h.001zm15.417 3.54l-5.42-3.088L14.896 7.6a.067.067 0 01.063-.006l4.489 2.557c1.998 1.14 2.683 3.662 1.529 5.633a4.163 4.163 0 01-2.174 1.807V12.38a.71.71 0 00-.363-.623zm1.867-2.773a6.04 6.04 0 00-.132-.078l-4.44-2.53a.731.731 0 00-.729 0l-5.42 3.088V7.325a.068.068 0 01.027-.057L14.1 4.713c2-1.137 4.555-.46 5.707 1.513.487.833.664 1.809.499 2.757h.001zm-11.741 3.81l-1.877-1.068a.065.065 0 01-.036-.051V6.559c.001-2.277 1.873-4.122 4.181-4.12.976 0 1.92.338 2.671.954-.034.018-.092.05-.131.073l-4.44 2.53a.71.71 0 00-.365.623l-.003 6.173v.002zm1.02-2.168L12 9.25l2.414 1.375v2.75L12 14.75l-2.415-1.375v-2.75z\"></path>\n  </svg>\n)\nexport default ChatGPTIcon\n"
  },
  {
    "path": "app/components/icons/ClaudeIcon.tsx",
    "content": "import React from 'react'\n\ninterface ClaudeIconProps extends React.SVGProps<SVGSVGElement> {\n  width?: number\n  height?: number\n  className?: string\n  style?: React.CSSProperties\n}\n\nexport default function ClaudeIcon({\n  width = 24,\n  height = 24,\n  className,\n  style,\n  ...props\n}: ClaudeIconProps) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 148.18 148.18\"\n      width={width}\n      height={height}\n      className={className}\n      style={style}\n      fill=\"none\"\n      {...props}\n    >\n      <g transform=\"translate(-75.96,-223.53)\">\n        <path\n          d=\"m 105.01,322.07 29.14,-16.35 0.49,-1.42 -0.49,-0.79 h -1.42 l -4.87,-0.3 -16.65,-0.45 -14.44,-0.6 -13.99,-0.75 -3.52,-0.75 -3.3,-4.35 0.34,-2.17 2.96,-1.99 4.24,0.37 9.37,0.64 14.06,0.97 10.2,0.6 15.11,1.57 h 2.4 l 0.34,-0.97 -0.82,-0.6 -0.64,-0.6 -14.55,-9.86 -15.75,-10.42 -8.25,-6 -4.46,-3.04 -2.25,-2.85 -0.97,-6.22 4.05,-4.46 5.44,0.37 1.39,0.37 5.51,4.24 11.77,9.11 15.37,11.32 2.25,1.87 0.9,-0.64 0.11,-0.45 -1.01,-1.69 -8.36,-15.11 -8.92,-15.37 -3.97,-6.37 -1.05,-3.82 c -0.37,-1.57 -0.64,-2.89 -0.64,-4.5 l 4.61,-6.26 2.55,-0.82 6.15,0.82 2.59,2.25 3.82,8.74 6.19,13.76 9.6,18.71 2.81,5.55 1.5,5.14 0.56,1.57 h 0.97 v -0.9 l 0.79,-10.54 1.46,-12.94 1.42,-16.65 0.49,-4.69 2.32,-5.62 4.61,-3.04 3.6,1.72 2.96,4.24 -0.41,2.74 -1.76,11.44 -3.45,17.92 -2.25,12 h 1.31 l 1.5,-1.5 6.07,-8.06 10.2,-12.75 4.5,-5.06 5.25,-5.59 3.37,-2.66 h 6.37 l 4.69,6.97 -2.1,7.2 -6.56,8.32 -5.44,7.05 -7.8,10.5 -4.87,8.4 0.45,0.67 1.16,-0.11 17.62,-3.75 9.52,-1.72 11.36,-1.95 5.14,2.4 0.56,2.44 -2.02,4.99 -12.15,3 -14.25,2.85 -21.22,5.02 -0.26,0.19 0.3,0.37 9.56,0.9 4.09,0.22 h 10.01 l 18.64,1.39 4.87,3.22 2.92,3.94 -0.49,3 -7.5,3.82 -10.12,-2.4 -23.62,-5.62 -8.1,-2.02 h -1.12 v 0.67 l 6.75,6.6 12.37,11.17 15.49,14.4 0.79,3.56 -1.99,2.81 -2.1,-0.3 -13.61,-10.24 -5.25,-4.61 -11.89,-10.01 h -0.79 v 1.05 l 2.74,4.01 14.47,21.75 0.75,6.67 -1.05,2.17 -3.75,1.31 -4.12,-0.75 -8.47,-11.89 -8.74,-13.39 -7.05,-12 -0.86,0.49 -4.16,44.81 -1.95,2.29 -4.5,1.72 -3.75,-2.85 -1.99,-4.61 1.99,-9.11 2.4,-11.89 1.95,-9.45 1.76,-11.74 1.05,-3.9 -0.07,-0.26 -0.86,0.11 -8.85,12.15 -13.46,18.19 -10.65,11.4 -2.55,1.01 -4.42,-2.29 0.41,-4.09 2.47,-3.64 14.74,-18.75 8.89,-11.62 5.74,-6.71 -0.04,-0.97 h -0.34 l -39.15,25.42 -6.97,0.9 -3,-2.81 0.37,-4.61 1.42,-1.5 11.77,-8.1 -0.04,0.04 z\"\n          fill=\"#d97757\"\n        />\n      </g>\n    </svg>\n  )\n}\n"
  },
  {
    "path": "app/components/icons/CodeWindowIcon.tsx",
    "content": "const CodeWindowIcon = props => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    data-name=\"Layer 1\"\n    viewBox=\"0 0 24 24\"\n    {...props}\n  >\n    <path d=\"M3 5.5a1.5 1.5 0 1 1 3.001.001A1.5 1.5 0 0 1 3 5.5zM8.5 7a1.5 1.5 0 1 0-.001-3.001A1.5 1.5 0 0 0 8.5 7zM24 6v12c0 2.757-2.243 5-5 5H5c-2.757 0-5-2.243-5-5V6c0-2.757 2.243-5 5-5h14c2.757 0 5 2.243 5 5zM2 6v2h20V6c0-1.654-1.346-3-3-3H5C3.346 3 2 4.346 2 6zm20 12v-8H2v8c0 1.654 1.346 3 3 3h14c1.654 0 3-1.346 3-3zm-11.793-4.793a.999.999 0 1 0-1.414-1.414l-2.181 2.181a2.243 2.243 0 0 0 .019 3.18l2.181 2.071a.999.999 0 1 0 1.377-1.449l-2.162-2.054a.237.237 0 0 1 0-.334l2.181-2.181zm5-1.414a.999.999 0 1 0-1.414 1.414l2.181 2.181c.092.092.092.242.011.323l-2.159 2.093a1 1 0 1 0 1.393 1.435l2.17-2.104a2.238 2.238 0 0 0 0-3.162l-2.181-2.181z\" />\n  </svg>\n)\nexport default CodeWindowIcon\n"
  },
  {
    "path": "app/components/icons/ColorSchemeIcon.tsx",
    "content": "const ColorSchemeIcon = props => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    data-name=\"Layer 1\"\n    viewBox=\"0 0 24 24\"\n    {...props}\n  >\n    <path d=\"m17,22.5c0,.828-.672,1.5-1.5,1.5s-1.5-.672-1.5-1.5.672-1.5,1.5-1.5,1.5.672,1.5,1.5Zm-8.5-1.5c-.828,0-1.5.672-1.5,1.5s.672,1.5,1.5,1.5,1.5-.672,1.5-1.5-.672-1.5-1.5-1.5Zm3.5-3.5c-.828,0-1.5.672-1.5,1.5s.672,1.5,1.5,1.5,1.5-.672,1.5-1.5-.672-1.5-1.5-1.5Zm9-9.5c0,.828.672,1.5,1.5,1.5s1.5-.672,1.5-1.5-.672-1.5-1.5-1.5-1.5.672-1.5,1.5Zm1.5,6.03c-.828,0-1.5.672-1.5,1.5s.672,1.5,1.5,1.5,1.5-.672,1.5-1.5-.672-1.5-1.5-1.5Zm-14.036,1.505c-1.949-1.95-1.949-5.122,0-7.071,1.949-1.949,5.123-1.949,7.072,0l1.061,1.061,4.949-4.95-2.121-2.121-2.928,2.928c-.92-.626-1.942-1.038-2.997-1.238V0h-3v4.144c-1.055.2-2.077.612-2.996,1.238l-2.929-2.928-2.121,2.121,2.929,2.928c-.625.92-1.038,1.942-1.238,2.996H0v3h4.145c.2,1.055.612,2.077,1.237,2.997l-2.928,2.928,2.121,2.121,4.949-4.95-1.061-1.061Zm14.036,5.464c-.828,0-1.5.672-1.5,1.5s.672,1.5,1.5,1.5,1.5-.672,1.5-1.5-.672-1.5-1.5-1.5Zm-3.5-10.5c-.828,0-1.5.672-1.5,1.5s.672,1.5,1.5,1.5,1.5-.672,1.5-1.5-.672-1.5-1.5-1.5Zm0,7c-.828,0-1.5.672-1.5,1.5s.672,1.5,1.5,1.5,1.5-.672,1.5-1.5-.672-1.5-1.5-1.5Zm-3.5-3.5c-.828,0-1.5.672-1.5,1.5s.672,1.5,1.5,1.5,1.5-.672,1.5-1.5-.672-1.5-1.5-1.5Z\" />\n  </svg>\n)\nexport default ColorSchemeIcon\n"
  },
  {
    "path": "app/components/icons/CursorIcon.tsx",
    "content": "const CursorIcon = _props => (\n  <svg\n    height=\"100%\"\n    style={{ flex: 'none', lineHeight: 1 }}\n    viewBox=\"0 0 24 24\"\n    width=\"100%\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <title>Cursor</title>\n    <path\n      d=\"M11.925 24l10.425-6-10.425-6L1.5 18l10.425 6z\"\n      fill=\"url(#lobe-icons-cursorundefined-fill-0)\"\n    ></path>\n    <path\n      d=\"M22.35 18V6L11.925 0v12l10.425 6z\"\n      fill=\"url(#lobe-icons-cursorundefined-fill-1)\"\n    ></path>\n    <path\n      d=\"M11.925 0L1.5 6v12l10.425-6V0z\"\n      fill=\"url(#lobe-icons-cursorundefined-fill-2)\"\n    ></path>\n    <path d=\"M22.35 6L11.925 24V12L22.35 6z\" fill=\"#555\"></path>\n    <path d=\"M22.35 6l-10.425 6L1.5 6h20.85z\" fill=\"#000\"></path>\n    <defs>\n      <linearGradient\n        gradientUnits=\"userSpaceOnUse\"\n        id=\"lobe-icons-cursorundefined-fill-0\"\n        x1=\"11.925\"\n        x2=\"11.925\"\n        y1=\"12\"\n        y2=\"24\"\n      >\n        <stop offset=\".16\" stopColor=\"#000\" stopOpacity=\".39\"></stop>\n        <stop offset=\".658\" stopColor=\"#000\" stopOpacity=\".8\"></stop>\n      </linearGradient>\n      <linearGradient\n        gradientUnits=\"userSpaceOnUse\"\n        id=\"lobe-icons-cursorundefined-fill-1\"\n        x1=\"22.35\"\n        x2=\"11.925\"\n        y1=\"6.037\"\n        y2=\"12.15\"\n      >\n        <stop offset=\".182\" stopColor=\"#000\" stopOpacity=\".31\"></stop>\n        <stop offset=\".715\" stopColor=\"#000\" stopOpacity=\"0\"></stop>\n      </linearGradient>\n      <linearGradient\n        gradientUnits=\"userSpaceOnUse\"\n        id=\"lobe-icons-cursorundefined-fill-2\"\n        x1=\"11.925\"\n        x2=\"1.5\"\n        y1=\"0\"\n        y2=\"18\"\n      >\n        <stop stopColor=\"#000\" stopOpacity=\".6\"></stop>\n        <stop offset=\".667\" stopColor=\"#000\" stopOpacity=\".22\"></stop>\n      </linearGradient>\n    </defs>\n  </svg>\n)\n\nexport default CursorIcon\n"
  },
  {
    "path": "app/components/icons/DiscordIcon.tsx",
    "content": "import React from 'react'\n\ninterface DiscordIconProps {\n  width?: number\n  height?: number\n  className?: string\n}\n\nexport default function DiscordIcon({\n  width = 24,\n  height = 24,\n  className,\n}: DiscordIconProps) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 -28.5 256 256\"\n      width={width}\n      height={height}\n      className={className}\n    >\n      <path\n        d=\"M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z\"\n        fill=\"currentColor\"\n        fillRule=\"nonzero\"\n      />\n    </svg>\n  )\n}\n"
  },
  {
    "path": "app/components/icons/FanIcon.tsx",
    "content": "const CodeWindowIcon = props => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    data-name=\"Layer 1\"\n    viewBox=\"0 0 24 24\"\n    {...props}\n  >\n    <path d=\"M22.941 9.755a4.11 4.11 0 0 0-2.858-1.369c-1.924-.104-4.141.528-5.678 1.083a3.453 3.453 0 0 0-.614-.46c1.244-1.303 2.769-2.901 2.863-4.645a4.111 4.111 0 0 0-1.053-2.989A4.112 4.112 0 0 0 12.744.006a4.099 4.099 0 0 0-2.989 1.052 4.113 4.113 0 0 0-1.368 2.857c-.106 1.924.527 4.142 1.083 5.679a3.507 3.507 0 0 0-.461.614C7.706 8.964 6.107 7.439 4.364 7.345a4.099 4.099 0 0 0-2.989 1.052 4.12 4.12 0 0 0-1.368 2.859c-.061 1.104.313 2.166 1.052 2.989s1.792 1.38 3.288 1.38c1.818 0 3.824-.58 5.248-1.095.186.177.393.328.614.461-1.244 1.303-2.769 2.901-2.863 4.645a4.111 4.111 0 0 0 1.053 2.989A4.11 4.11 0 0 0 11.483 24a4.145 4.145 0 0 0 4.131-3.916c.106-1.924-.527-4.142-1.083-5.679.177-.186.328-.393.461-.614 1.303 1.244 2.902 2.769 4.646 2.863a4.145 4.145 0 0 0 4.357-3.91 4.106 4.106 0 0 0-1.052-2.989ZM4.024 13.616a2.129 2.129 0 0 1-1.478-.708 2.129 2.129 0 0 1-.543-1.545c.03-.571.281-1.096.707-1.477.396-.355.753-.544 1.545-.544 1.007 0 2.197 1.192 3.249 2.196.343.328.695.656 1.046.961.013.087.019.175.038.26-1.335.46-3.092.932-4.564.856Zm7.067-11.069A2.124 2.124 0 0 1 12.517 2c.697 0 1.215.286 1.597.711.381.425.574.974.543 1.545-.054 1.005-1.192 2.198-2.196 3.25-.327.343-.656.694-.961 1.045-.087.013-.175.019-.26.038-.461-1.335-.935-3.094-.855-4.565.03-.571.281-1.096.707-1.477ZM13.5 12c0 .827-.673 1.5-1.5 1.5s-1.5-.673-1.5-1.5.673-1.5 1.5-1.5 1.5.673 1.5 1.5Zm-.591 9.453a2.149 2.149 0 0 1-1.545.544 2.129 2.129 0 0 1-1.478-.708 2.127 2.127 0 0 1-.543-1.545c.054-1.005 1.192-2.198 2.196-3.25.327-.343.656-.694.961-1.045.087-.013.175-.019.26-.038.461 1.335.935 3.094.855 4.565a2.12 2.12 0 0 1-.707 1.477Zm6.835-6.796c-1.006-.054-2.197-1.192-3.249-2.196a30.096 30.096 0 0 0-1.046-.961c-.013-.087-.019-.175-.038-.26 1.335-.461 3.088-.936 4.564-.856.57.031 1.096.282 1.478.708.381.426.574.974.543 1.545a2.152 2.152 0 0 1-2.252 2.021Z\" />\n  </svg>\n)\nexport default CodeWindowIcon\n"
  },
  {
    "path": "app/components/icons/GitHubIcon.tsx",
    "content": "import React from 'react'\n\ninterface GitHubIconProps {\n  width?: number\n  height?: number\n  className?: string\n}\n\nexport default function GitHubIcon({\n  width = 16,\n  height = 16,\n  className,\n}: GitHubIconProps) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 50 50\"\n      width={width}\n      height={height}\n      className={className}\n      fill=\"currentColor\"\n    >\n      <path d=\"M17.791,46.836C18.502,46.53,19,45.823,19,45v-5.4c0-0.197,0.016-0.402,0.041-0.61C19.027,38.994,19.014,38.997,19,39 c0,0-3,0-3.6,0c-1.5,0-2.8-0.6-3.4-1.8c-0.7-1.3-1-3.5-2.8-4.7C8.9,32.3,9.1,32,9.7,32c0.6,0.1,1.9,0.9,2.7,2c0.9,1.1,1.8,2,3.4,2 c2.487,0,3.82-0.125,4.622-0.555C21.356,34.056,22.649,33,24,33v-0.025c-5.668-0.182-9.289-2.066-10.975-4.975 c-3.665,0.042-6.856,0.405-8.677,0.707c-0.058-0.327-0.108-0.656-0.151-0.987c1.797-0.296,4.843-0.647,8.345-0.714 c-0.112-0.276-0.209-0.559-0.291-0.849c-3.511-0.178-6.541-0.039-8.187,0.097c-0.02-0.332-0.047-0.663-0.051-0.999 c1.649-0.135,4.597-0.27,8.018-0.111c-0.079-0.5-0.13-1.011-0.13-1.543c0-1.7,0.6-3.5,1.7-5c-0.5-1.7-1.2-5.3,0.2-6.6 c2.7,0,4.6,1.3,5.5,2.1C21,13.4,22.9,13,25,13s4,0.4,5.6,1.1c0.9-0.8,2.8-2.1,5.5-2.1c1.5,1.4,0.7,5,0.2,6.6c1.1,1.5,1.7,3.2,1.6,5 c0,0.484-0.045,0.951-0.11,1.409c3.499-0.172,6.527-0.034,8.204,0.102c-0.002,0.337-0.033,0.666-0.051,0.999 c-1.671-0.138-4.775-0.28-8.359-0.089c-0.089,0.336-0.197,0.663-0.325,0.98c3.546,0.046,6.665,0.389,8.548,0.689 c-0.043,0.332-0.093,0.661-0.151,0.987c-1.912-0.306-5.171-0.664-8.879-0.682C35.112,30.873,31.557,32.75,26,32.969V33 c2.6,0,5,3.9,5,6.6V45c0,0.823,0.498,1.53,1.209,1.836C41.37,43.804,48,35.164,48,25C48,12.318,37.683,2,25,2S2,12.318,2,25 C2,35.164,8.63,43.804,17.791,46.836z\" />\n    </svg>\n  )\n}\n"
  },
  {
    "path": "app/components/icons/GmailIcon.tsx",
    "content": "interface GmailIconProps {\n  className?: string\n}\n\nconst GmailIcon = ({ className }: GmailIconProps) => (\n  <svg\n    fill=\"none\"\n    height=\"100%\"\n    viewBox=\"0 0 53.3208 36.6354\"\n    width=\"100%\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    className={className}\n  >\n    <path\n      d=\"m3.6354 36.6354h8.4828v-20.6008l-12.1182-9.0887v26.0541c0 2.0116 1.6299 3.6354 3.6354 3.6354z\"\n      fill=\"#4285f4\"\n    />\n    <path\n      d=\"m41.2031 36.6354h8.4827c2.0117 0 3.6355-1.6298 3.6355-3.6354v-26.0541l-12.1182 9.0887\"\n      fill=\"#34a853\"\n    />\n    <path\n      d=\"m41.2031 0.2812v15.7536l12.1182-9.0886v-4.8473c0-4.4959-5.1321-7.0588-8.7251-4.3626\"\n      fill=\"#fbbc04\"\n    />\n    <path\n      d=\"m12.1172 16.0345v-15.7536l14.5418 10.9064 14.5418-10.9064v15.7536l-14.5418 10.9064\"\n      fill=\"#ea4335\"\n    />\n    <path\n      d=\"m0 2.0989v4.8473l12.1182 9.0886v-15.7536l-3.3931-2.5449c-3.5991-2.6962-8.7251-.1333-8.7251 4.3626z\"\n      fill=\"#c5221f\"\n    />\n  </svg>\n)\n\nexport default GmailIcon\n"
  },
  {
    "path": "app/components/icons/GoogleIcon.tsx",
    "content": "import React from 'react'\n\ninterface GoogleIconProps {\n  width?: number\n  height?: number\n  className?: string\n}\n\nexport default function GoogleIcon({\n  width = 20,\n  height = 20,\n  className,\n}: GoogleIconProps) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 48 48\"\n      width={width}\n      height={height}\n      className={className}\n    >\n      <path\n        fill=\"#FFC107\"\n        d=\"M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12c0-6.627,5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24c0,11.045,8.955,20,20,20c11.045,0,20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z\"\n      />\n      <path\n        fill=\"#FF3D00\"\n        d=\"M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z\"\n      />\n      <path\n        fill=\"#4CAF50\"\n        d=\"M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z\"\n      />\n      <path\n        fill=\"#1976D2\"\n        d=\"M43.611,20.083H42V20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z\"\n      />\n    </svg>\n  )\n}\n"
  },
  {
    "path": "app/components/icons/IMessageIcon.tsx",
    "content": "import React from 'react'\n\ninterface IMessageIconProps extends React.SVGProps<SVGSVGElement> {\n  width?: number\n  height?: number\n  className?: string\n  style?: React.CSSProperties\n}\n\nexport default function IMessageIcon({\n  width = 24,\n  height = 24,\n  className,\n  style,\n  ...props\n}: IMessageIconProps) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={width}\n      height={height}\n      viewBox=\"0 0 66.145836 66.145836\"\n      className={className}\n      style={style}\n      fill=\"none\"\n      {...props}\n    >\n      <defs>\n        <linearGradient\n          id=\"imessage-gradient\"\n          x1=\"-25.272568\"\n          y1=\"207.52057\"\n          x2=\"-25.272568\"\n          y2=\"152.9982\"\n          gradientUnits=\"userSpaceOnUse\"\n        >\n          <stop offset=\"0\" stopColor=\"#0cbd2a\" />\n          <stop offset=\"1\" stopColor=\"#5bf675\" />\n        </linearGradient>\n      </defs>\n      <rect\n        ry=\"14.567832\"\n        rx=\"14.567832\"\n        y=\"0\"\n        x=\"0\"\n        height=\"66.145836\"\n        width=\"66.145836\"\n        style={{ fill: 'url(#imessage-gradient)' }}\n      />\n      <path\n        d=\"M32.072918,11.45046a24.278298,20.222157 0 0 0-24.278105,20.22202 24.278298,20.222157 0 0 0 11.79463,17.31574 27.365264,20.222157 0 0 1-4.245218,5.94228 23.85735,20.222157 0 0 0 9.86038-3.87367 24.278298,20.222157 0 0 0 6.868313,0.83768 24.278298,20.222157 0 0 0 24.278106-20.22203 24.278298,20.222157 0 0 0-24.278106-20.22202z\"\n        style={{ fill: '#fff' }}\n      />\n    </svg>\n  )\n}\n"
  },
  {
    "path": "app/components/icons/ItoIcon.tsx",
    "content": "import React from 'react'\n\ninterface ItoIconProps {\n  className?: string\n  style?: React.CSSProperties\n  width?: number\n  height?: number\n}\n\nexport const ItoIcon: React.FC<ItoIconProps> = ({\n  className = '',\n  width = 141,\n  height = 141,\n  style,\n}) => {\n  return (\n    <svg\n      width={width}\n      height={height}\n      viewBox=\"0 0 141 141\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      className={className}\n      style={style}\n    >\n      <path\n        d=\"M125.837 88.3633C129.501 84.6822 132.622 80.4752 135.037 75.8738C138.947 68.4622 141 60.1469 141 51.7657C141 23.2206 117.787 0 89.2524 0C60.7172 0 37.5047 23.2206 37.5047 51.7657C37.5047 55.3482 37.8661 58.8322 38.5561 62.201C44.3058 62.4147 50.0227 63.8115 55.296 66.3752C53.3576 61.8888 52.2733 56.9423 52.2733 51.7493C52.2733 31.3552 68.849 14.7738 89.2359 14.7738C109.623 14.7738 126.199 31.3552 126.199 51.7493C126.199 61.9381 122.059 71.1902 115.373 77.8951C100.965 92.3073 77.5065 92.3073 63.0993 77.8951C48.6921 63.4829 25.2331 63.4829 10.8424 77.8951C4.15624 84.5836 0 93.8357 0 104.024C0 124.419 16.5757 141 36.9626 141C57.3495 141 72.6767 125.618 73.8431 106.276C68.5369 104.797 63.4278 102.513 58.6637 99.4724C58.9759 100.951 59.1402 102.463 59.1402 104.024C59.1402 116.251 49.1849 126.21 36.9626 126.21C24.7403 126.21 14.785 116.251 14.785 104.024C14.785 97.9112 17.2656 92.3566 21.274 88.3304C29.9151 79.6864 43.9937 79.6864 52.6347 88.3304C62.7214 98.4206 75.9787 103.466 89.2195 103.466C98.5669 103.466 107.865 100.919 115.882 96.1035C119.496 93.9343 122.831 91.3049 125.804 88.3304L125.837 88.3633Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  )\n}\n\nexport default ItoIcon\n"
  },
  {
    "path": "app/components/icons/MicrosoftIcon.tsx",
    "content": "import React from 'react'\n\ninterface MicrosoftIconProps {\n  width?: number\n  height?: number\n  className?: string\n}\n\nexport default function MicrosoftIcon({\n  width = 20,\n  height = 20,\n  className,\n}: MicrosoftIconProps) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      x=\"0px\"\n      y=\"0px\"\n      width={width}\n      height={height}\n      viewBox=\"0 0 48 48\"\n      className={className}\n    >\n      <path\n        fill=\"#ff5722\"\n        d=\"M6 6H22V22H6z\"\n        transform=\"rotate(-180 14 14)\"\n      ></path>\n      <path\n        fill=\"#4caf50\"\n        d=\"M26 6H42V22H26z\"\n        transform=\"rotate(-180 34 14)\"\n      ></path>\n      <path\n        fill=\"#ffc107\"\n        d=\"M26 26H42V42H26z\"\n        transform=\"rotate(-180 34 34)\"\n      ></path>\n      <path\n        fill=\"#03a9f4\"\n        d=\"M6 26H22V42H6z\"\n        transform=\"rotate(-180 14 34)\"\n      ></path>\n    </svg>\n  )\n}\n"
  },
  {
    "path": "app/components/icons/NotionIcon.tsx",
    "content": "const NotionIcon = ({ className }: { className?: string }) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    x=\"0px\"\n    y=\"0px\"\n    width=\"100%\"\n    height=\"100%\"\n    viewBox=\"0 0 40 40.8613\"\n    className={className}\n  >\n    <path d=\"M 26.494141 0.1503906 L 0.9277344 2.0019531 A 1.0001 1.0001 0 0 0 0.9042969 2.0039062 A 1.0001 1.0001 0 0 0 0.8652344 2.0097656 A 1.0001 1.0001 0 0 0 0.7929688 2.0214844 A 1.0001 1.0001 0 0 0 0.7636719 2.0292969 A 1.0001 1.0001 0 0 0 0.7304688 2.0371094 A 1.0001 1.0001 0 0 0 0.6582031 2.0605469 A 1.0001 1.0001 0 0 0 0.6113281 2.0800781 A 1.0001 1.0001 0 0 0 0.5839844 2.0917969 A 1.0001 1.0001 0 0 0 0.4335938 2.1777344 A 1.0001 1.0001 0 0 0 0.4082031 2.1933594 A 1.0001 1.0001 0 0 0 0.3476562 2.2421875 A 1.0001 1.0001 0 0 0 0.3359375 2.2539062 A 1.0001 1.0001 0 0 0 0.2871094 2.2988281 A 1.0001 1.0001 0 0 0 0.2578125 2.3320312 A 1.0001 1.0001 0 0 0 0.2148438 2.3828125 A 1.0001 1.0001 0 0 0 0.1992188 2.4023438 A 1.0001 1.0001 0 0 0 0.15625 2.4648438 A 1.0001 1.0001 0 0 0 0.1445312 2.484375 A 1.0001 1.0001 0 0 0 0.1074219 2.5488281 A 1.0001 1.0001 0 0 0 0.09375 2.5761719 A 1.0001 1.0001 0 0 0 0.0644531 2.6484375 A 1.0001 1.0001 0 0 0 0.0605469 2.65625 A 1.0001 1.0001 0 0 0 0.015625 2.8300781 A 1.0001 1.0001 0 0 0 0.0097656 2.8613281 A 1.0001 1.0001 0 0 0 0.0019531 2.9414062 A 1.0001 1.0001 0 0 0 0.0019531 2.9453125 A 1.0001 1.0001 0 0 0 0 3 L 0 28.738281 C 0 29.76391 0.3151542 30.766862 0.9042969 31.607422 A 1.0001 1.0001 0 0 0 0.953125 31.671875 L 7.126953 39.101562 A 1.0001 1.0001 0 0 0 7.359375 39.382812 L 7.75 39.851562 A 1.0006635 1.0006635 0 0 0 7.917969 40.011719 C 8.50508 40.581386 9.317167 40.917563 10.193359 40.861328 L 37.193359 39.119141 C 38.762433 39.017718 40 37.697027 40 36.125 L 40 10.132812 C 40 9.209354 39.565523 8.390672 38.904297 7.839844 A 1.0008168 1.0008168 0 0 0 38.748047 7.695312 L 38.263672 7.337891 A 1.0001 1.0001 0 0 0 38.0625 7.189453 L 29.824219 1.1132812 C 28.865071 0.4054876 27.682705 0.0641541 26.494141 0.1503906 z M 26.638672 2.1445312 C 27.352108 2.0927682 28.061867 2.29845 28.636719 2.7226562 L 34.767578 7.246094 L 9.742188 8.884766 C 8.880567 8.941006 8.037689 8.622196 7.425781 8.011719 L 7.423828 8.011719 L 3.2539062 3.8398438 L 26.638672 2.1445312 z M 2 5.414062 L 6.011719 9.425781 L 7 10.414062 L 7 35.818359 L 2.5390625 30.449219 C 2.1899317 29.947488 2 29.351269 2 28.738281 L 2 5.414062 z M 36.935547 9.134766 C 37.526748 9.096822 38 9.54116 38 10.132812 L 38 36.125 C 38 36.660973 37.59938 37.08847 37.064453 37.123047 L 10.064453 38.865234 C 9.770856 38.884078 9.506356 38.783483 9.314453 38.605469 A 1.0006635 1.0006635 0 0 0 9.3125 38.603516 C 9.3125 38.603516 9.310547 38.601562 9.310547 38.601562 C 9.306465 38.597733 9.304796 38.59179 9.300781 38.587891 A 1.0006635 1.0006635 0 0 0 9.289062 38.572266 C 9.112238 38.393435 9 38.149431 9 37.867188 L 9 11.875 C 9 11.337536 9.39999 10.911571 9.935547 10.876953 L 36.935547 9.134766 z M 33.496094 14 L 28.421875 14.28125 C 27.647875 14.36125 26.746094 14.938 26.746094 15.875 L 28.996094 16.0625 L 28.996094 26.753906 L 21.214844 14.751953 L 15.382812 15.080078 C 14.291812 15.160078 13.994141 15.970953 13.994141 17.001953 L 16.244141 17.001953 L 16.244141 32.566406 C 16.244141 32.566406 15.191844 32.850406 14.839844 32.941406 C 14.091844 33.134406 13.994141 33.784906 13.994141 34.253906 C 13.994141 34.253906 17.746656 34.065547 19.472656 33.935547 C 21.431656 33.785547 21.496094 32.472656 21.496094 32.472656 L 19.246094 32.003906 L 19.246094 20.470703 C 19.246094 20.470703 24.965844 29.660328 26.714844 32.361328 C 27.537844 33.630328 28.152375 33.878906 29.234375 33.878906 C 30.122375 33.878906 30.962141 33.616594 31.994141 33.058594 L 31.994141 15.697266 C 31.994141 15.697266 32.184203 15.687141 32.783203 15.494141 C 33.466203 15.273141 33.496094 14.656 33.496094 14 z\"></path>\n  </svg>\n)\nexport default NotionIcon\n"
  },
  {
    "path": "app/components/icons/SlackIcon.tsx",
    "content": "interface SlackIconProps {\n  className?: string\n}\n\nconst SlackIcon = ({ className }: SlackIconProps) => (\n  <svg\n    height=\"100%\"\n    preserveAspectRatio=\"xMidYMid\"\n    viewBox=\"0 0 256 256\"\n    width=\"100%\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    className={className}\n  >\n    <path\n      d=\"m53.8412698 161.320635c0 14.831746-11.9873015 26.819048-26.8190476 26.819048-14.831746 0-26.8190476-11.987302-26.8190476-26.819048s11.9873016-26.819048 26.8190476-26.819048h26.8190476zm13.4095239 0c0-14.831746 11.9873015-26.819048 26.8190476-26.819048 14.8317457 0 26.8190477 11.987302 26.8190477 26.819048v67.047619c0 14.831746-11.987302 26.819048-26.8190477 26.819048-14.8317461 0-26.8190476-11.987302-26.8190476-26.819048z\"\n      fill=\"#e01e5a\"\n    />\n    <path\n      d=\"m94.0698413 53.6380952c-14.8317461 0-26.8190476-11.9873015-26.8190476-26.8190476 0-14.831746 11.9873015-26.8190476 26.8190476-26.8190476 14.8317457 0 26.8190477 11.9873016 26.8190477 26.8190476v26.8190476zm0 13.6126985c14.8317457 0 26.8190477 11.9873015 26.8190477 26.8190476 0 14.8317457-11.987302 26.8190477-26.8190477 26.8190477h-67.2507937c-14.831746 0-26.8190476-11.987302-26.8190476-26.8190477 0-14.8317461 11.9873016-26.8190476 26.8190476-26.8190476z\"\n      fill=\"#36c5f0\"\n    />\n    <path\n      d=\"m201.549206 94.0698413c0-14.8317461 11.987302-26.8190476 26.819048-26.8190476s26.819048 11.9873015 26.819048 26.8190476c0 14.8317457-11.987302 26.8190477-26.819048 26.8190477h-26.819048zm-13.409523 0c0 14.8317457-11.987302 26.8190477-26.819048 26.8190477s-26.819048-11.987302-26.819048-26.8190477v-67.2507937c0-14.831746 11.987302-26.8190476 26.819048-26.8190476s26.819048 11.9873016 26.819048 26.8190476z\"\n      fill=\"#2eb67d\"\n    />\n    <path\n      d=\"m161.320635 201.549206c14.831746 0 26.819048 11.987302 26.819048 26.819048s-11.987302 26.819048-26.819048 26.819048-26.819048-11.987302-26.819048-26.819048v-26.819048zm0-13.409523c-14.831746 0-26.819048-11.987302-26.819048-26.819048s11.987302-26.819048 26.819048-26.819048h67.250794c14.831746 0 26.819047 11.987302 26.819047 26.819048s-11.987301 26.819048-26.819047 26.819048z\"\n      fill=\"#ecb22e\"\n    />\n  </svg>\n)\nexport default SlackIcon\n"
  },
  {
    "path": "app/components/icons/SpeedIcon.tsx",
    "content": "export const SpeedIcon = () => {\n  return (\n    <svg\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M15.793 6.79297C16.1835 6.40249 16.8165 6.40246 17.207 6.79297C17.5975 7.18348 17.5975 7.81652 17.207 8.20703L13.9297 11.4834C13.9738 11.6484 14 11.8211 14 12C14 13.1046 13.1046 14 12 14C10.8954 14 10 13.1046 10 12C10 10.8954 10.8954 10 12 10C12.1785 10 12.351 10.0255 12.5156 10.0693L15.793 6.79297Z\"\n        fill=\"#59C581\"\n      />\n      <path\n        d=\"M12 4C12.5523 4 13 4.44772 13 5C13 5.55228 12.5523 6 12 6C8.68629 6 6 8.68629 6 12C6 12.5523 5.55228 13 5 13C4.44772 13 4 12.5523 4 12C4 7.58173 7.58173 4 12 4Z\"\n        fill=\"#59C581\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M12 1C18.0751 1 23 5.92486 23 12C23 18.0751 18.0751 23 12 23C5.92486 23 1 18.0751 1 12C1 5.92487 5.92487 1 12 1ZM12 3C7.02943 3 3 7.02943 3 12C3 16.9705 7.02944 21 12 21C16.9705 21 21 16.9705 21 12C21 7.02944 16.9705 3 12 3Z\"\n        fill=\"#59C581\"\n      />\n    </svg>\n  )\n}\n"
  },
  {
    "path": "app/components/icons/TotalWordsIcon.tsx",
    "content": "export const TotalWordsIcon = () => {\n  return (\n    <svg\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M14 17.5V6.5C14 5.94772 14.4477 5.5 15 5.5C15.5523 5.5 16 5.94772 16 6.5V17.5C16 18.0523 15.5523 18.5 15 18.5C14.4477 18.5 14 18.0523 14 17.5Z\"\n        fill=\"#DA8231\"\n      />\n      <path\n        d=\"M11 13.5V10.5C11 9.94772 11.4477 9.5 12 9.5C12.5523 9.5 13 9.94772 13 10.5V13.5C13 14.0523 12.5523 14.5 12 14.5C11.4477 14.5 11 14.0523 11 13.5Z\"\n        fill=\"#DA8231\"\n      />\n      <path\n        d=\"M17 13V11C17 10.4477 17.4477 10 18 10C18.5523 10 19 10.4477 19 11V13C19 13.5523 18.5523 14 18 14C17.4477 14 17 13.5523 17 13Z\"\n        fill=\"#DA8231\"\n      />\n      <path\n        d=\"M8 14.5V9.5C8 8.94772 8.44772 8.5 9 8.5C9.55228 8.5 10 8.94772 10 9.5V14.5C10 15.0523 9.55228 15.5 9 15.5C8.44772 15.5 8 15.0523 8 14.5Z\"\n        fill=\"#DA8231\"\n      />\n      <path\n        d=\"M5 13V11C5 10.4477 5.44772 10 6 10C6.55228 10 7 10.4477 7 11V13C7 13.5523 6.55228 14 6 14C5.44772 14 5 13.5523 5 13Z\"\n        fill=\"#DA8231\"\n      />\n      <path\n        d=\"M20 8C20 5.79086 18.2091 4 16 4H8C5.79086 4 4 5.79086 4 8V16C4 18.2091 5.79086 20 8 20H16C18.2091 20 20 18.2091 20 16V8ZM22 16C22 19.3137 19.3137 22 16 22H8C4.68629 22 2 19.3137 2 16V8C2 4.68629 4.68629 2 8 2H16C19.3137 2 22 4.68629 22 8V16Z\"\n        fill=\"#DA8231\"\n      />\n    </svg>\n  )\n}\n"
  },
  {
    "path": "app/components/icons/VSCodeIcon.tsx",
    "content": "import React from 'react'\n\ninterface VSCodeIconProps extends React.SVGProps<SVGSVGElement> {\n  width?: number\n  height?: number\n  className?: string\n  style?: React.CSSProperties\n}\n\nexport default function VSCodeIcon({\n  width = 24,\n  height = 24,\n  className,\n  style,\n  ...props\n}: VSCodeIconProps) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 48 48\"\n      width={width}\n      height={height}\n      className={className}\n      style={style}\n      fill=\"none\"\n      {...props}\n    >\n      <path\n        fill=\"#29b6f6\"\n        d=\"M44,11.11v25.78c0,1.27-0.79,2.4-1.98,2.82l-8.82,4.14L34,33V15L33.2,4.15l8.82,4.14 C43.21,8.71,44,9.84,44,11.11z\"\n      />\n      <path\n        fill=\"#0277bd\"\n        d=\"M9,33.896L34,15V5.353c0-1.198-1.482-1.758-2.275-0.86L4.658,29.239 c-0.9,0.83-0.849,2.267,0.107,3.032c0,0,1.324,1.232,1.803,1.574C7.304,34.37,8.271,34.43,9,33.896z\"\n      />\n      <path\n        fill=\"#0288d1\"\n        d=\"M9,14.104L34,33v9.647c0,1.198-1.482,1.758-2.275,0.86L4.658,18.761 c-0.9-0.83-0.849-2.267,0.107-3.032c0,0,1.324-1.232,1.803-1.574C7.304,13.63,8.271,13.57,9,14.104z\"\n      />\n    </svg>\n  )\n}\n"
  },
  {
    "path": "app/components/icons/XIcon.tsx",
    "content": "import React from 'react'\n\ninterface XIconProps {\n  width?: number\n  height?: number\n  className?: string\n}\n\nexport default function XIcon({\n  width = 24,\n  height = 24,\n  className,\n}: XIconProps) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 16 16\"\n      width={width}\n      height={height}\n      className={className}\n      fill=\"currentColor\"\n    >\n      <path d=\"M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865z\" />\n    </svg>\n  )\n}\n"
  },
  {
    "path": "app/components/pill/Pill.tsx",
    "content": "import React, { useState, useEffect, useRef } from 'react'\nimport { useSettingsStore } from '../../store/useSettingsStore'\nimport {\n  useOnboardingStore,\n  ONBOARDING_CATEGORIES,\n} from '../../store/useOnboardingStore'\nimport { Tooltip, TooltipTrigger, TooltipContent } from '../ui/tooltip'\nimport { X, StopSquare } from '@mynaui/icons-react'\nimport { AudioBars } from './contents/AudioBars'\nimport { PreviewAudioBars } from './contents/PreviewAudioBars'\nimport { LoadingAnimation } from './contents/LoadingAnimation'\nimport { useAudioStore } from '@/app/store/useAudioStore'\nimport { TooltipButton } from './contents/TooltipButton'\nimport { analytics, ANALYTICS_EVENTS } from '../analytics'\nimport type {\n  RecordingStatePayload,\n  ProcessingStatePayload,\n} from '@/lib/types/ipc'\nimport { ItoMode } from '@/app/generated/ito_pb'\n\nconst globalStyles = `\n  html, body, #app {\n    height: 100%;\n    margin: 0;\n    overflow: hidden; /* Prevent scrollbars */\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n\n    /* These styles are key to anchoring the pill to the bottom center */\n    /* of its transparent window, allowing it to expand upwards. */\n    display: flex;\n    align-items: flex-end;\n    justify-content: center;\n\n    pointer-events: none;\n\n    font-family:\n      'Inter',\n      system-ui,\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Roboto,\n      sans-serif;\n  }\n`\n\nconst BAR_UPDATE_INTERVAL = 64\n\n// Color mapping for different recording modes\nconst getAudioBarColor = (mode: ItoMode | undefined): string => {\n  switch (mode) {\n    case ItoMode.TRANSCRIBE:\n      return 'white'\n    case ItoMode.EDIT:\n      return '#FFCF40'\n    default:\n      return 'white' // Default to white for transcribe mode\n  }\n}\n\nconst Pill = () => {\n  // Get initial values from store using separate selectors to avoid infinite re-renders\n  const initialShowItoBarAlways = useSettingsStore(\n    state => state.showItoBarAlways,\n  )\n  const initialOnboardingCategory = useOnboardingStore(\n    state => state.onboardingCategory,\n  )\n  const initialOnboardingCompleted = useOnboardingStore(\n    state => state.onboardingCompleted,\n  )\n  const { startRecording, stopRecording } = useAudioStore()\n\n  const [isRecording, setIsRecording] = useState(false)\n  const [isManualRecording, setIsManualRecording] = useState(false)\n  const [isProcessing, setIsProcessing] = useState(false)\n  const [isHovered, setIsHovered] = useState(false)\n  const [recordingMode, setRecordingMode] = useState<ItoMode | undefined>()\n  const isManualRecordingRef = useRef(false)\n  const [showItoBarAlways, setShowItoBarAlways] = useState(\n    initialShowItoBarAlways,\n  )\n  const [onboardingCategory, setOnboardingCategory] = useState(\n    initialOnboardingCategory,\n  )\n  const [onboardingCompleted, setOnboardingCompleted] = useState(\n    initialOnboardingCompleted,\n  )\n  // Fixed size array of volume values to be used for the audio bars, size is 21\n  const [volumeHistory, setVolumeHistory] = useState<number[]>([])\n  const [lastVolumeUpdate, setLastVolumeUpdate] = useState(0)\n\n  useEffect(() => {\n    // Listen for recording state changes from the main process\n    const unsubRecording = window.api.on(\n      'recording-state-update',\n      (state: RecordingStatePayload) => {\n        // Update recording state - this is for global hotkey triggered recording\n        setIsRecording(state.isRecording)\n        setRecordingMode(state.mode ?? recordingMode)\n\n        // Only track general recording analytics if it's not a manual recording\n        if (!isManualRecordingRef.current) {\n          const analyticsEvent = state.isRecording\n            ? ANALYTICS_EVENTS.RECORDING_STARTED\n            : ANALYTICS_EVENTS.RECORDING_COMPLETED\n          analytics.track(analyticsEvent, {\n            is_recording: state.isRecording,\n            mode: state.mode,\n          })\n        }\n\n        // If global recording stops, also stop manual recording\n        if (!state.isRecording) {\n          setIsManualRecording(false)\n          isManualRecordingRef.current = false\n          // Only clear volume history when recording stops\n          setVolumeHistory([])\n        }\n      },\n    )\n\n    // Listen for processing state changes from the main process\n    const unsubProcessing = window.api.on(\n      'processing-state-update',\n      (state: ProcessingStatePayload) => {\n        setIsProcessing(state.isProcessing)\n      },\n    )\n\n    // Listen for volume updates from the main process\n    const unsubVolume = window.api.on('volume-update', (vol: number) => {\n      // throttle the volume updates to 80ms\n      const now = Date.now()\n      if (now - lastVolumeUpdate < BAR_UPDATE_INTERVAL) {\n        return\n      }\n      const newVolumeHistory = [...volumeHistory, vol]\n      if (newVolumeHistory.length > 42) {\n        newVolumeHistory.shift()\n      }\n      setVolumeHistory(newVolumeHistory)\n      setLastVolumeUpdate(now)\n    })\n\n    // Listen for settings updates from the main process\n    const unsubSettings = window.api.on('settings-update', (settings: any) => {\n      // Update local state with the new setting\n      setShowItoBarAlways(settings.showItoBarAlways)\n    })\n\n    // Listen for onboarding updates from the main process\n    const unsubOnboarding = window.api.on(\n      'onboarding-update',\n      (onboarding: any) => {\n        setOnboardingCategory(onboarding.onboardingCategory)\n        setOnboardingCompleted(onboarding.onboardingCompleted)\n      },\n    )\n\n    // Listen for user auth updates from the main process\n    const unsubUserAuth = window.api.on('user-auth-update', (authUser: any) => {\n      if (authUser) {\n        analytics.identifyUser(\n          authUser.id,\n          {\n            user_id: authUser.id,\n            email: authUser.email,\n            name: authUser.name,\n            provider: authUser.provider,\n          },\n          authUser.provider,\n        )\n      } else {\n        // User logged out\n        analytics.resetUser()\n      }\n    })\n\n    // Cleanup listeners when the component unmounts\n    return () => {\n      unsubRecording()\n      unsubProcessing()\n      unsubVolume()\n      unsubSettings()\n      unsubOnboarding()\n      unsubUserAuth()\n    }\n  }, [volumeHistory, lastVolumeUpdate, recordingMode])\n\n  // Define dimensions for different states\n  const idleWidth = 36\n  const idleHeight = 8\n  const hoveredWidth = 84\n  const hoveredHeight = 32\n  const recordingWidth = 84\n  const recordingHeight = 32\n  const manualRecordingWidth = 112\n  const manualRecordingHeight = 32\n  const processingWidth = 84\n  const processingHeight = 32\n\n  // Determine current state\n  const anyRecording = isRecording || isManualRecording\n  const shouldShow =\n    (onboardingCategory === ONBOARDING_CATEGORIES.TRY_IT ||\n      onboardingCompleted) &&\n    (anyRecording || isProcessing || showItoBarAlways || isHovered)\n\n  // Calculate dimensions based on state\n  let currentWidth = idleWidth\n  let currentHeight = idleHeight\n  let backgroundColor = 'rgba(128, 128, 128, 0.65)'\n\n  if (isManualRecording) {\n    currentWidth = manualRecordingWidth\n    currentHeight = manualRecordingHeight\n    backgroundColor = '#000000'\n  } else if (anyRecording) {\n    currentWidth = recordingWidth\n    currentHeight = recordingHeight\n    backgroundColor = '#000000'\n  } else if (isProcessing) {\n    currentWidth = processingWidth\n    currentHeight = processingHeight\n    backgroundColor = '#000000'\n  } else if (isHovered) {\n    currentWidth = hoveredWidth\n    currentHeight = hoveredHeight\n    backgroundColor = '#404040'\n  }\n\n  // A single, unified style for the pill. Its properties will be\n  // smoothly transitioned by CSS.\n  const pillStyle: React.CSSProperties = {\n    // Flex properties to center the content inside\n    display: 'flex',\n    alignItems: 'center',\n    justifyContent: 'center',\n\n    // Dynamic styles that change based on the state\n    width: `${currentWidth}px`,\n    height: `${currentHeight}px`,\n    backgroundColor,\n    border: '1px solid #A9A9A9',\n\n    // Show/hide animation using opacity and scale instead of display none/flex\n    opacity: shouldShow ? 1 : 0,\n    transform: shouldShow ? 'scale(1)' : 'scale(0.8)',\n    transformOrigin: 'bottom center',\n    visibility: shouldShow ? 'visible' : 'hidden',\n\n    // Static styles\n    borderRadius: '21px',\n    boxSizing: 'border-box',\n    overflow: 'hidden',\n\n    // Enable pointer events for this element\n    pointerEvents: 'auto',\n    cursor: isHovered && !anyRecording ? 'pointer' : 'default',\n\n    // The transition property makes the magic happen!\n    // We animate width, height, color, opacity, and scale changes over 0.3 seconds.\n    transition:\n      'width 0.3s ease, height 0.3s ease, background-color 0.3s ease, opacity 0.3s ease, transform 0.3s ease, visibility 0.3s ease',\n  }\n\n  // Handle mouse enter - enable mouse events for the pill window and set hover state\n  const handleMouseEnter = () => {\n    setIsHovered(true)\n    if (window.api?.setPillMouseEvents) {\n      window.api.setPillMouseEvents(false) // Enable mouse events\n    }\n  }\n\n  // Handle mouse leave - disable mouse events (with forwarding) for the pill window and clear hover state\n  const handleMouseLeave = () => {\n    setIsHovered(false)\n    if (window.api?.setPillMouseEvents) {\n      window.api.setPillMouseEvents(true, { forward: true }) // Disable mouse events but keep forwarding\n    }\n  }\n\n  // Handle click to start manual recording\n  const handleClick = () => {\n    if (isHovered && !anyRecording) {\n      setIsManualRecording(true)\n      isManualRecordingRef.current = true\n      // Trigger recording start via IPC\n      startRecording()\n\n      analytics.track(ANALYTICS_EVENTS.MANUAL_RECORDING_STARTED, {\n        is_recording: true,\n      })\n    }\n  }\n\n  // Handle cancel recording\n  const handleCancel = (e: React.MouseEvent) => {\n    e.stopPropagation()\n    setIsManualRecording(false)\n    stopRecording()\n\n    analytics.track(ANALYTICS_EVENTS.MANUAL_RECORDING_ABANDONED, {\n      is_recording: false,\n    })\n  }\n\n  // Handle stop recording\n  const handleStop = (e: React.MouseEvent) => {\n    e.stopPropagation()\n    setIsManualRecording(false)\n    stopRecording()\n\n    analytics.track(ANALYTICS_EVENTS.MANUAL_RECORDING_COMPLETED, {\n      is_recording: false,\n    })\n  }\n\n  const renderContent = () => {\n    if (isManualRecording) {\n      return (\n        <div\n          style={{\n            display: 'flex',\n            alignItems: 'center',\n            width: '100%',\n            justifyContent: 'space-between',\n            padding: '0 8px',\n          }}\n        >\n          <TooltipButton\n            onClick={handleCancel}\n            icon={<X width={14} height={14} color=\"white\" />}\n            tooltip=\"Cancel\"\n          />\n\n          <AudioBars\n            volumeHistory={volumeHistory}\n            barColor={getAudioBarColor(recordingMode)}\n          />\n\n          <TooltipButton\n            onClick={handleStop}\n            icon={<StopSquare width={14} height={14} color=\"#ef4444\" />}\n            tooltip=\"Stop and paste\"\n          />\n        </div>\n      )\n    }\n\n    if (anyRecording) {\n      return (\n        <AudioBars\n          volumeHistory={volumeHistory}\n          barColor={getAudioBarColor(recordingMode)}\n        />\n      )\n    }\n\n    if (isProcessing) {\n      return <LoadingAnimation color={getAudioBarColor(recordingMode)} />\n    }\n\n    if (isHovered) {\n      return <PreviewAudioBars />\n    }\n\n    return null\n  }\n\n  return (\n    <>\n      <style>{globalStyles}</style>\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <div\n            style={pillStyle}\n            onClick={handleClick}\n            onMouseEnter={handleMouseEnter}\n            onMouseLeave={handleMouseLeave}\n          >\n            {renderContent()}\n          </div>\n        </TooltipTrigger>\n        {isHovered && !anyRecording && (\n          <TooltipContent\n            side=\"top\"\n            style={{\n              backgroundColor: 'rgba(0, 0, 0, 0.8)',\n              color: 'white',\n              padding: '6px 8px',\n              fontSize: '14px',\n              marginBottom: '6px',\n              borderRadius: '8px',\n            }}\n            className=\"border-none rounded-md\"\n          >\n            Click and start speaking\n          </TooltipContent>\n        )}\n      </Tooltip>\n    </>\n  )\n}\n\nexport default Pill\n"
  },
  {
    "path": "app/components/pill/contents/AudioBars.tsx",
    "content": "import { useEffect, useState } from 'react'\nimport { AudioBarsBase, BAR_COUNT } from './AudioBarsBase'\n\n// A new component to very basic audio visualization\nexport const AudioBars = ({\n  volumeHistory,\n  barColor = 'white',\n}: {\n  volumeHistory: number[]\n  barColor?: string\n}) => {\n  // Base heights for visual variety\n  const bars = Array(BAR_COUNT).fill(1)\n  const [activeBarIndex, setActiveBarIndex] = useState(0)\n\n  useEffect(() => {\n    setActiveBarIndex(prevIndex => (prevIndex + 1) % bars.length)\n  }, [volumeHistory, bars.length])\n\n  // Calculate dynamic heights based on volume and active bar\n  const dynamicHeights = bars.map((baseHeight, index) => {\n    const volume = volumeHistory[volumeHistory.length - index - 1] || 0\n    const scale = Math.max(0.05, Math.min(1, volume * 20))\n    const activeBarHeight = index === activeBarIndex ? 2 : 0\n    const height = activeBarHeight + baseHeight * 20 * scale\n    return Math.min(Math.max(height, 1), 16)\n  })\n\n  return <AudioBarsBase heights={dynamicHeights} barColor={barColor} />\n}\n"
  },
  {
    "path": "app/components/pill/contents/AudioBarsBase.tsx",
    "content": "interface AudioBarsBaseProps {\n  heights: number[]\n  barColor: string\n}\n\nexport const BAR_COUNT = 21\n\nexport const AudioBarsBase = ({ heights, barColor }: AudioBarsBaseProps) => {\n  const barStyle = (height: number): React.CSSProperties => {\n    return {\n      width: '2px',\n      backgroundColor: barColor,\n      borderRadius: '2.5px',\n      margin: '0 0.25px',\n      height: `${height}px`,\n    }\n  }\n\n  return (\n    <div\n      style={{\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center',\n        height: '100%',\n      }}\n    >\n      {heights.map((height, i) => (\n        <div key={i} style={barStyle(height)} />\n      ))}\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/pill/contents/LoadingAnimation.tsx",
    "content": "import React from 'react'\n\ninterface LoadingAnimationProps {\n  color?: string\n}\n\nexport const LoadingAnimation: React.FC<LoadingAnimationProps> = ({\n  color = '#FFFFFF',\n}) => {\n  return (\n    <>\n      <style>{`\n        @keyframes shadowRolling {\n          0% {\n            box-shadow: 0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0);\n          }\n          12% {\n            box-shadow: 100px 0 ${color}, 0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0);\n          }\n          25% {\n            box-shadow: 110px 0 ${color}, 100px 0 ${color}, 0px 0 rgba(255, 255, 255, 0), 0px 0 rgba(255, 255, 255, 0);\n          }\n          36% {\n            box-shadow: 120px 0 ${color}, 110px 0 ${color}, 100px 0 ${color}, 0px 0 rgba(255, 255, 255, 0);\n          }\n          50% {\n            box-shadow: 130px 0 ${color}, 120px 0 ${color}, 110px 0 ${color}, 100px 0 ${color};\n          }\n          62% {\n            box-shadow: 200px 0 rgba(255, 255, 255, 0), 130px 0 ${color}, 120px 0 ${color}, 110px 0 ${color};\n          }\n          75% {\n            box-shadow: 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0), 130px 0 ${color}, 120px 0 ${color};\n          }\n          87% {\n            box-shadow: 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0), 130px 0 ${color};\n          }\n          100% {\n            box-shadow: 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0), 200px 0 rgba(255, 255, 255, 0);\n          }\n        }\n      `}</style>\n      <span\n        style={{\n          width: '8px',\n          height: '8px',\n          borderRadius: '50%',\n          display: 'block',\n          position: 'relative',\n          color: color,\n          left: '-100px',\n          boxSizing: 'border-box',\n          animation: 'shadowRolling 2s linear infinite',\n        }}\n      />\n    </>\n  )\n}\n"
  },
  {
    "path": "app/components/pill/contents/PreviewAudioBars.tsx",
    "content": "import { AudioBarsBase } from './AudioBarsBase'\n\nexport const PreviewAudioBars = () => {\n  // Create varied static heights for a nice preview effect\n  const staticHeights = [\n    3, 7, 4, 9, 12, 6, 8, 11, 5, 14, 6, 1, 1, 9, 15, 11, 7, 13, 9, 3, 2,\n  ]\n\n  return <AudioBarsBase heights={staticHeights} barColor=\"#FFFFFF\" />\n}\n"
  },
  {
    "path": "app/components/pill/contents/TooltipButton.tsx",
    "content": "import {\n  Tooltip,\n  TooltipTrigger,\n  TooltipContent,\n} from '@/app/components/ui/tooltip'\n\ninterface TooltipButtonProps {\n  onClick: (e: React.MouseEvent) => void\n  icon: React.ReactNode\n  tooltip: string\n}\n\nexport const TooltipButton: React.FC<TooltipButtonProps> = ({\n  onClick,\n  icon,\n  tooltip,\n}) => (\n  <Tooltip>\n    <TooltipTrigger asChild>\n      <button\n        onClick={onClick}\n        style={{\n          background: 'none',\n          border: 'none',\n          cursor: 'pointer',\n          display: 'flex',\n          alignItems: 'center',\n          padding: '4px',\n        }}\n      >\n        {icon}\n      </button>\n    </TooltipTrigger>\n    <TooltipContent\n      side=\"top\"\n      style={{\n        backgroundColor: 'rgba(0, 0, 0, 0.8)',\n        color: 'white',\n        padding: '6px 8px',\n        fontSize: '14px',\n        marginBottom: '6px',\n      }}\n      className=\"border-none rounded-md\"\n    >\n      {tooltip}\n    </TooltipContent>\n  </Tooltip>\n)\n"
  },
  {
    "path": "app/components/ui/animated-checkmark.tsx",
    "content": "import { useState, useEffect, useRef } from 'react'\nimport { Check } from '@mynaui/icons-react'\n\n// AnimatedCheck component for check mark animation\nfunction AnimatedCheck({ trigger }: { trigger: boolean }) {\n  const [showWidth, setShowWidth] = useState(false)\n  const [showOpacity, setShowOpacity] = useState(false)\n  const checkRef = useRef<HTMLDivElement>(null)\n\n  useEffect(() => {\n    if (trigger) {\n      setShowWidth(false)\n      setShowOpacity(false)\n      // Start width animation\n      setTimeout(() => {\n        setShowWidth(true)\n        // After width animation, start opacity\n        setTimeout(() => {\n          setShowOpacity(true)\n        }, 350) // match transition duration\n      }, 50)\n    } else {\n      setShowWidth(false)\n      setShowOpacity(false)\n    }\n  }, [trigger])\n\n  return (\n    <div\n      ref={checkRef}\n      style={{\n        overflow: 'hidden',\n        display: 'inline-block',\n        width: showWidth ? 24 : 0,\n        transition: 'width 0.35s cubic-bezier(0.4,0,0.2,1)',\n        verticalAlign: 'middle',\n      }}\n    >\n      <Check\n        className=\"mr-1\"\n        style={{\n          color: '#22c55e',\n          width: 24,\n          height: 24,\n          opacity: showOpacity ? 1 : 0,\n          transition: 'opacity 0.25s cubic-bezier(0.4,0,0.2,1)',\n        }}\n      />\n    </div>\n  )\n}\n\nexport { AnimatedCheck }\n"
  },
  {
    "path": "app/components/ui/app-orbit-image.tsx",
    "content": "import ItoIcon from '../icons/ItoIcon'\nimport GitHubIcon from '../icons/GitHubIcon'\nimport NotionIcon from '../icons/NotionIcon'\nimport SlackIcon from '../icons/SlackIcon'\nimport CursorIcon from '../icons/CursorIcon'\nimport AppleNotesIcon from '../icons/AppleNotesIcon'\nimport ChatGPTIcon from '../icons/ChatGPTIcon'\nimport IMessageIcon from '../icons/IMessageIcon'\nimport GmailIcon from '../icons/GmailIcon'\nimport VSCodeIcon from '../icons/VSCodeIcon'\nimport ClaudeIcon from '../icons/ClaudeIcon'\n\nimport React from 'react'\n\nconst orbitIcons = [\n  {\n    icon: <AppleNotesIcon className=\"size-16\" />,\n    bg: '#fff',\n    angle: 10,\n    radius: 280,\n    rotation: 12,\n  },\n  {\n    icon: <ClaudeIcon className=\"size-16\" />,\n    bg: '#fff',\n    angle: 40,\n    radius: 280,\n    rotation: -15,\n  },\n  {\n    icon: <ChatGPTIcon className=\"size-16\" />,\n    bg: '#fff',\n    angle: 160,\n    radius: 280,\n    rotation: 18,\n  },\n  {\n    icon: <SlackIcon className=\"size-16\" />,\n    bg: '#fff',\n    angle: 200,\n    radius: 280,\n    rotation: -10,\n  },\n  {\n    icon: <IMessageIcon className=\"size-16\" />,\n    bg: '#fff',\n    angle: 240,\n    radius: 280,\n    rotation: 20,\n  },\n  {\n    icon: <VSCodeIcon className=\"size-16\" />,\n    bg: '#fff',\n    angle: 310,\n    radius: 280,\n    rotation: -8,\n  },\n  {\n    icon: <CursorIcon className=\"size-16\" />,\n    bg: '#fff',\n    angle: 340,\n    radius: 280,\n    rotation: 14,\n  },\n]\n\nconst innerOrbitIcons = [\n  {\n    icon: <GmailIcon className=\"size-16\" />,\n    bg: '#fff',\n    angle: 0,\n    radius: 180,\n    rotation: -18,\n  },\n  {\n    icon: <NotionIcon className=\"size-16\" />,\n    bg: '#fff',\n    angle: 130,\n    radius: 180,\n    rotation: 16,\n  },\n  {\n    icon: <GitHubIcon className=\"size-16\" />,\n    bg: '#fff',\n    angle: 220,\n    radius: 180,\n    rotation: -20,\n  },\n]\n\nexport function AppOrbitImage() {\n  return (\n    <div className=\"relative w-[560px] h-[560px] flex items-center justify-center select-none\">\n      {/* Concentric Circles */}\n      <div className=\"absolute inset-0 flex items-center justify-center pointer-events-none z-0\">\n        {/* Outer orbit circle with horizontal gradient fade (top/bottom faded) */}\n        <svg\n          width=\"560\"\n          height=\"560\"\n          className=\"absolute\"\n          style={{ left: 0, top: 0 }}\n        >\n          <circle\n            cx=\"280\"\n            cy=\"280\"\n            r=\"279\"\n            fill=\"none\"\n            stroke=\"url(#outerGradient)\"\n            strokeWidth=\"1\"\n            opacity=\"0.5\"\n          />\n          <defs>\n            <linearGradient\n              id=\"outerGradient\"\n              x1=\"0\"\n              y1=\"280\"\n              x2=\"560\"\n              y2=\"280\"\n              gradientUnits=\"userSpaceOnUse\"\n            >\n              <stop stopColor=\"#A8A8A8\" />\n              <stop offset=\"0.278846\" stopColor=\"#A8A8A8\" stopOpacity=\"0\" />\n              <stop offset=\"0.6875\" stopColor=\"#A8A8A8\" stopOpacity=\"0\" />\n              <stop offset=\"1\" stopColor=\"#A8A8A8\" />\n            </linearGradient>\n          </defs>\n        </svg>\n        {/* Inner orbit circle with horizontal gradient fade (top/bottom faded) */}\n        <svg\n          width=\"360\"\n          height=\"360\"\n          className=\"absolute\"\n          style={{ left: 100, top: 100 }}\n        >\n          <circle\n            cx=\"180\"\n            cy=\"180\"\n            r=\"179\"\n            fill=\"none\"\n            stroke=\"url(#innerGradient)\"\n            strokeWidth=\"1\"\n            opacity=\"0.5\"\n          />\n          <defs>\n            <linearGradient\n              id=\"innerGradient\"\n              x1=\"0\"\n              y1=\"180\"\n              x2=\"360\"\n              y2=\"180\"\n              gradientUnits=\"userSpaceOnUse\"\n            >\n              <stop stopColor=\"#A8A8A8\" />\n              <stop offset=\"0.278846\" stopColor=\"#A8A8A8\" stopOpacity=\"0\" />\n              <stop offset=\"0.6875\" stopColor=\"#A8A8A8\" stopOpacity=\"0\" />\n              <stop offset=\"1\" stopColor=\"#A8A8A8\" />\n            </linearGradient>\n          </defs>\n        </svg>\n        {/* Center Ito icon circle (provided SVG, horizontal gradient fade) */}\n        <svg\n          width=\"195\"\n          height=\"196\"\n          viewBox=\"0 0 195 196\"\n          fill=\"none\"\n          className=\"absolute\"\n          style={{ left: 182.5, top: 182 }}\n        >\n          <circle\n            opacity=\"0.5\"\n            cx=\"97.6547\"\n            cy=\"98.0002\"\n            r=\"96.9619\"\n            transform=\"rotate(0 97.6547 98.0002)\"\n            stroke=\"url(#itoGradient)\"\n            strokeWidth=\"0.609703\"\n          />\n          <defs>\n            <linearGradient\n              id=\"itoGradient\"\n              x1=\"0\"\n              y1=\"98\"\n              x2=\"195\"\n              y2=\"98\"\n              gradientUnits=\"userSpaceOnUse\"\n            >\n              <stop stopColor=\"#A8A8A8\" />\n              <stop offset=\"0.278846\" stopColor=\"#A8A8A8\" stopOpacity=\"0\" />\n              <stop offset=\"0.6875\" stopColor=\"#A8A8A8\" stopOpacity=\"0\" />\n              <stop offset=\"1\" stopColor=\"#A8A8A8\" />\n            </linearGradient>\n          </defs>\n        </svg>\n      </div>\n      {/* Outer orbit icons */}\n      {orbitIcons.map(({ icon, angle, radius, bg, rotation }, i) => {\n        const rad = (angle * Math.PI) / 180\n        const x = Math.cos(rad) * radius\n        const y = Math.sin(rad) * radius\n        return (\n          <div\n            key={`outer-${i}`}\n            className=\"absolute\"\n            style={{\n              left: `calc(50% + ${x}px - 32px)`,\n              top: `calc(50% + ${y}px - 32px)`,\n            }}\n          >\n            <div\n              className=\"rounded-xl shadow-md flex items-center justify-center p-2\"\n              style={{\n                background: bg,\n                width: 64,\n                height: 64,\n                transform: `rotate(${rotation}deg)`,\n              }}\n            >\n              {icon}\n            </div>\n          </div>\n        )\n      })}\n      {/* Inner orbit icons */}\n      {innerOrbitIcons.map(({ icon, angle, radius, bg, rotation }, i) => {\n        const rad = (angle * Math.PI) / 180\n        const x = Math.cos(rad) * radius\n        const y = Math.sin(rad) * radius\n        return (\n          <div\n            key={`inner-${i}`}\n            className=\"absolute\"\n            style={{\n              left: `calc(50% + ${x}px - 32px)`,\n              top: `calc(50% + ${y}px - 32px)`,\n            }}\n          >\n            <div\n              className=\"rounded-xl shadow flex items-center justify-center p-2\"\n              style={{\n                background: bg,\n                width: 64,\n                height: 64,\n                transform: `rotate(${rotation}deg)`,\n              }}\n            >\n              {icon}\n            </div>\n          </div>\n        )\n      })}\n      {/* Center Ito icon */}\n      <div className=\"absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-black rounded-full w-20 h-20 flex items-center justify-center shadow-lg\">\n        <ItoIcon height={48} width={48} style={{ color: '#fff' }} />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/ui/badge.tsx",
    "content": "import * as React from 'react'\nimport { Slot } from '@radix-ui/react-slot'\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nimport { cn } from '@/lib/utils'\n\nconst badgeVariants = cva(\n  'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',\n  {\n    variants: {\n      variant: {\n        default:\n          'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',\n        secondary:\n          'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',\n        destructive:\n          'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',\n        outline:\n          'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n    },\n  },\n)\n\nfunction Badge({\n  className,\n  variant,\n  asChild = false,\n  ...props\n}: React.ComponentProps<'span'> &\n  VariantProps<typeof badgeVariants> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot : 'span'\n\n  return (\n    <Comp\n      data-slot=\"badge\"\n      className={cn(badgeVariants({ variant }), className)}\n      {...props}\n    />\n  )\n}\n\nexport { Badge, badgeVariants }\n"
  },
  {
    "path": "app/components/ui/button.tsx",
    "content": "import * as React from 'react'\nimport { Slot } from '@radix-ui/react-slot'\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nimport { cn } from '@/lib/utils'\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n  {\n    variants: {\n      variant: {\n        default:\n          'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',\n        destructive:\n          'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',\n        outline:\n          'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',\n        secondary:\n          'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',\n        ghost:\n          'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',\n        link: 'text-primary underline-offset-4 hover:underline',\n      },\n      size: {\n        default: 'h-9 px-4 py-2 has-[>svg]:px-3',\n        sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',\n        lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',\n        icon: 'size-9',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      size: 'default',\n    },\n  },\n)\n\nfunction Button({\n  className,\n  variant,\n  size,\n  asChild = false,\n  ...props\n}: React.ComponentProps<'button'> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean\n  }) {\n  const Comp = asChild ? Slot : 'button'\n\n  return (\n    <Comp\n      data-slot=\"button\"\n      className={cn(buttonVariants({ variant, size, className }))}\n      {...props}\n    />\n  )\n}\n\nexport { Button, buttonVariants }\n"
  },
  {
    "path": "app/components/ui/dialog.tsx",
    "content": "import * as React from 'react'\nimport * as DialogPrimitive from '@radix-ui/react-dialog'\nimport { XIcon } from 'lucide-react'\n\nimport { cn } from '@/lib/utils'\n\nfunction Dialog({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Root>) {\n  return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />\n}\n\nfunction DialogTrigger({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n  return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />\n}\n\nfunction DialogPortal({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n  return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />\n}\n\nfunction DialogClose({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Close>) {\n  return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />\n}\n\nfunction DialogOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {\n  return (\n    <DialogPrimitive.Overlay\n      data-slot=\"dialog-overlay\"\n      className={cn(\n        'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction DialogContent({\n  className,\n  children,\n  showCloseButton = true,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & {\n  showCloseButton?: boolean\n}) {\n  return (\n    <DialogPortal data-slot=\"dialog-portal\">\n      <DialogOverlay />\n      <DialogPrimitive.Content\n        data-slot=\"dialog-content\"\n        className={cn(\n          'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        {showCloseButton && (\n          <DialogPrimitive.Close\n            data-slot=\"dialog-close\"\n            className=\"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n          >\n            <XIcon />\n            <span className=\"sr-only\">Close</span>\n          </DialogPrimitive.Close>\n        )}\n      </DialogPrimitive.Content>\n    </DialogPortal>\n  )\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {\n  return (\n    <div\n      data-slot=\"dialog-header\"\n      className={cn('flex flex-col gap-2 text-center sm:text-left', className)}\n      {...props}\n    />\n  )\n}\n\nfunction DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {\n  return (\n    <div\n      data-slot=\"dialog-footer\"\n      className={cn(\n        'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction DialogTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Title>) {\n  return (\n    <DialogPrimitive.Title\n      data-slot=\"dialog-title\"\n      className={cn('text-lg leading-none font-semibold', className)}\n      {...props}\n    />\n  )\n}\n\nfunction DialogDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n  return (\n    <DialogPrimitive.Description\n      data-slot=\"dialog-description\"\n      className={cn('text-muted-foreground text-sm', className)}\n      {...props}\n    />\n  )\n}\n\nexport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogOverlay,\n  DialogPortal,\n  DialogTitle,\n  DialogTrigger,\n}\n"
  },
  {
    "path": "app/components/ui/dropdown-menu.tsx",
    "content": "import * as React from 'react'\nimport * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'\nimport { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'\n\nimport { cn } from '@/lib/utils'\n\nfunction DropdownMenu({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {\n  return <DropdownMenuPrimitive.Root data-slot=\"dropdown-menu\" {...props} />\n}\n\nfunction DropdownMenuPortal({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {\n  return (\n    <DropdownMenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />\n  )\n}\n\nfunction DropdownMenuTrigger({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {\n  return (\n    <DropdownMenuPrimitive.Trigger\n      data-slot=\"dropdown-menu-trigger\"\n      {...props}\n    />\n  )\n}\n\nfunction DropdownMenuContent({\n  className,\n  sideOffset = 4,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n  return (\n    <DropdownMenuPrimitive.Portal>\n      <DropdownMenuPrimitive.Content\n        data-slot=\"dropdown-menu-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',\n          className,\n        )}\n        {...props}\n      />\n    </DropdownMenuPrimitive.Portal>\n  )\n}\n\nfunction DropdownMenuGroup({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {\n  return (\n    <DropdownMenuPrimitive.Group data-slot=\"dropdown-menu-group\" {...props} />\n  )\n}\n\nfunction DropdownMenuItem({\n  className,\n  inset,\n  variant = 'default',\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {\n  inset?: boolean\n  variant?: 'default' | 'destructive'\n}) {\n  return (\n    <DropdownMenuPrimitive.Item\n      data-slot=\"dropdown-menu-item\"\n      data-inset={inset}\n      data-variant={variant}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction DropdownMenuCheckboxItem({\n  className,\n  children,\n  checked,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {\n  return (\n    <DropdownMenuPrimitive.CheckboxItem\n      data-slot=\"dropdown-menu-checkbox-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      checked={checked}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CheckIcon className=\"size-4\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.CheckboxItem>\n  )\n}\n\nfunction DropdownMenuRadioGroup({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {\n  return (\n    <DropdownMenuPrimitive.RadioGroup\n      data-slot=\"dropdown-menu-radio-group\"\n      {...props}\n    />\n  )\n}\n\nfunction DropdownMenuRadioItem({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {\n  return (\n    <DropdownMenuPrimitive.RadioItem\n      data-slot=\"dropdown-menu-radio-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CircleIcon className=\"size-2 fill-current\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.RadioItem>\n  )\n}\n\nfunction DropdownMenuLabel({\n  className,\n  inset,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {\n  inset?: boolean\n}) {\n  return (\n    <DropdownMenuPrimitive.Label\n      data-slot=\"dropdown-menu-label\"\n      data-inset={inset}\n      className={cn(\n        'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction DropdownMenuSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n  return (\n    <DropdownMenuPrimitive.Separator\n      data-slot=\"dropdown-menu-separator\"\n      className={cn('bg-border -mx-1 my-1 h-px', className)}\n      {...props}\n    />\n  )\n}\n\nfunction DropdownMenuShortcut({\n  className,\n  ...props\n}: React.ComponentProps<'span'>) {\n  return (\n    <span\n      data-slot=\"dropdown-menu-shortcut\"\n      className={cn(\n        'text-muted-foreground ml-auto text-xs tracking-widest',\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction DropdownMenuSub({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {\n  return <DropdownMenuPrimitive.Sub data-slot=\"dropdown-menu-sub\" {...props} />\n}\n\nfunction DropdownMenuSubTrigger({\n  className,\n  inset,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {\n  inset?: boolean\n}) {\n  return (\n    <DropdownMenuPrimitive.SubTrigger\n      data-slot=\"dropdown-menu-sub-trigger\"\n      data-inset={inset}\n      className={cn(\n        'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronRightIcon className=\"ml-auto size-4\" />\n    </DropdownMenuPrimitive.SubTrigger>\n  )\n}\n\nfunction DropdownMenuSubContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n  return (\n    <DropdownMenuPrimitive.SubContent\n      data-slot=\"dropdown-menu-sub-content\"\n      className={cn(\n        'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nexport {\n  DropdownMenu,\n  DropdownMenuPortal,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuLabel,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuSub,\n  DropdownMenuSubTrigger,\n  DropdownMenuSubContent,\n}\n"
  },
  {
    "path": "app/components/ui/keyboard-key.tsx",
    "content": "import { ComponentPropsWithoutRef } from 'react'\nimport clsx from 'clsx'\nimport { cx } from 'class-variance-authority'\nimport { KeyName, getKeyDisplayInfo } from '../../../lib/types/keyboard'\nimport { getDirectionalIndicator, getKeyDisplay } from '../../utils/keyboard'\nimport { usePlatform } from '../../hooks/usePlatform'\n\nconst FnKey = () => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    viewBox=\"0 0 80 80\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <g transform=\"translate(15, 46)\">\n      <circle cx=\"9\" cy=\"9\" r=\"8\" fill=\"none\" stroke=\"#666\" strokeWidth=\"1.5\" />\n      <line x1=\"9\" y1=\"1\" x2=\"9\" y2=\"17\" stroke=\"#666\" strokeWidth=\"1.2\" />\n      <line x1=\"1\" y1=\"9\" x2=\"17\" y2=\"9\" stroke=\"#666\" strokeWidth=\"1.2\" />\n      <path\n        d=\"M9 1 C4.5 4.5 4.5 13.5 9 17\"\n        fill=\"none\"\n        stroke=\"#666\"\n        strokeWidth=\"1\"\n      />\n      <path\n        d=\"M9 1 C13.5 4.5 13.5 13.5 9 17\"\n        fill=\"none\"\n        stroke=\"#666\"\n        strokeWidth=\"1\"\n      />\n      <path\n        d=\"M2.5 5.5 C5.5 4.5 12.5 4.5 15.5 5.5\"\n        fill=\"none\"\n        stroke=\"#666\"\n        strokeWidth=\"1\"\n      />\n      <path\n        d=\"M2.5 12.5 C5.5 13.5 12.5 13.5 15.5 12.5\"\n        fill=\"none\"\n        stroke=\"#666\"\n        strokeWidth=\"1\"\n      />\n    </g>\n    <text\n      x=\"56\"\n      y=\"28\"\n      fontFamily=\"SF Pro Display, -apple-system, BlinkMacSystemFont, sans-serif\"\n      fontSize=\"16\"\n      fontWeight=\"400\"\n      fill=\"#333\"\n      textAnchor=\"middle\"\n    >\n      fn\n    </text>\n  </svg>\n)\n\nconst ModifierKey = ({\n  keyboardKey,\n  symbol,\n  side,\n  showDirectionalText = false,\n}: {\n  keyboardKey: string\n  symbol: string\n  side?: 'left' | 'right'\n  showDirectionalText?: boolean\n}) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    viewBox=\"0 0 80 80\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    {/* Symbol at the top */}\n    <text\n      x=\"40\"\n      y=\"22\"\n      fontFamily=\"SF Pro Display, -apple-system, BlinkMacSystemFont, sans-serif\"\n      fontSize=\"18\"\n      fontWeight=\"400\"\n      fill=\"#666\"\n      textAnchor=\"middle\"\n    >\n      {symbol}\n    </text>\n\n    {/* Name in the middle */}\n    <text\n      x=\"40\"\n      y=\"45\"\n      fontFamily=\"SF Pro Display, -apple-system, BlinkMacSystemFont, sans-serif\"\n      fontSize=\"12\"\n      fontWeight=\"400\"\n      fill=\"#666\"\n      textAnchor=\"middle\"\n    >\n      {keyboardKey}\n    </text>\n\n    {/* Direction indicator at the bottom */}\n    {side && (\n      <text\n        x=\"40\"\n        y=\"62\"\n        fontFamily=\"SF Pro Display, -apple-system, BlinkMacSystemFont, sans-serif\"\n        fontSize=\"10\"\n        fontWeight=\"400\"\n        fill=\"#888\"\n        textAnchor=\"middle\"\n      >\n        {getDirectionalIndicator(side, showDirectionalText)}\n      </text>\n    )}\n  </svg>\n)\n\nconst DefaultKey = ({ keyboardKey }: { keyboardKey: string }) => {\n  let label = keyboardKey\n  if (/^[a-zA-Z]$/.test(label)) label = label.toUpperCase()\n  let fontSize = 20\n  if (label.length > 3) fontSize = 18\n  if (label.length > 6) fontSize = 16\n  return (\n    <svg\n      width=\"100%\"\n      height=\"100%\"\n      viewBox=\"0 0 80 80\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <text\n        x=\"40\"\n        y=\"44\"\n        fontFamily=\"SF Pro Display, -apple-system, BlinkMacSystemFont, sans-serif\"\n        fontSize={fontSize}\n        fontWeight=\"400\"\n        fill=\"#666\"\n        textAnchor=\"middle\"\n      >\n        {label}\n      </text>\n    </svg>\n  )\n}\n\nconst KeyToRender = ({\n  keyboardKey,\n  showDirectionalText = false,\n  platform = 'darwin',\n}: {\n  keyboardKey: KeyName\n  showDirectionalText?: boolean\n  platform?: 'darwin' | 'win32'\n}) => {\n  if (keyboardKey === 'fn' || keyboardKey === 'fn_fast') {\n    return <FnKey />\n  }\n\n  const displayInfo = getKeyDisplayInfo(keyboardKey, platform)\n\n  if (displayInfo.isModifier && displayInfo.symbol) {\n    return (\n      <ModifierKey\n        keyboardKey={displayInfo.label}\n        symbol={displayInfo.symbol}\n        side={displayInfo.side}\n        showDirectionalText={showDirectionalText}\n      />\n    )\n  }\n\n  return <DefaultKey keyboardKey={keyboardKey} />\n}\n\n/* ---------------- Component ---------------- */\n\ninterface KeyboardKeyProps extends ComponentPropsWithoutRef<'div'> {\n  keyboardKey: KeyName\n  /** 'tile' = big square SVG (default). 'inline' = small pill for rows/inline usage. */\n  variant?: 'tile' | 'inline'\n  /** Optional compact size for the tile variant */\n  size?: 'md' | 'sm'\n  /** Whether to show directional text (left/right) for modifier keys. Default: false */\n  showDirectionalText?: boolean\n}\n\nexport default function KeyboardKey({\n  keyboardKey,\n  className,\n  variant = 'tile',\n  showDirectionalText = false,\n  ...props\n}: KeyboardKeyProps) {\n  const platform = usePlatform()\n\n  if (variant === 'inline') {\n    const display = getKeyDisplay(keyboardKey, platform, {\n      showDirectionalText,\n      format: 'symbol',\n    })\n\n    return (\n      <span\n        className={clsx(\n          'inline-flex select-none items-center justify-center rounded-xl border border-neutral-300',\n          'bg-neutral-100 px-2.5 py-1 text-sm leading-5 text-neutral-900 shadow-sm',\n          className,\n        )}\n        {...props}\n      >\n        {display}\n      </span>\n    )\n  }\n\n  return (\n    <div className={cx('rounded-lg shadow-lg', className)} {...props}>\n      <KeyToRender\n        keyboardKey={keyboardKey}\n        showDirectionalText={showDirectionalText}\n        platform={platform}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/ui/keyboard-shortcut-editor.tsx",
    "content": "import { useEffect, useCallback, useRef, useState, useMemo } from 'react'\nimport { Button } from '@/app/components/ui/button'\nimport KeyboardKey from '@/app/components/ui/keyboard-key'\nimport { KeyState, isReservedCombination } from '@/app/utils/keyboard'\nimport { keyNameMap } from '@/lib/types/keyboard'\nimport { useAudioStore } from '@/app/store/useAudioStore'\nimport { KeyboardShortcutConfig } from './multi-shortcut-editor'\nimport { KeyName } from '@/lib/types/keyboard'\nimport { usePlatform } from '@/app/hooks/usePlatform'\nimport { useShortcutEditingStore } from '@/app/store/useShortcutEditingStore'\n\ninterface KeyboardShortcutEditorProps {\n  shortcut: KeyboardShortcutConfig\n  onShortcutChange: (shortcutId: string, newShortcutKeys: string[]) => void\n  hideTitle?: boolean\n  className?: string\n  keySize?: number\n  editButtonText?: string\n  confirmButtonText?: string\n  showConfirmButton?: boolean\n  onConfirm?: () => void\n  editModeTitle?: string\n  viewModeTitle?: string\n  minHeight?: number\n  editButtonClassName?: string\n  confirmButtonClassName?: string\n}\n\nconst MAX_KEYS_PER_SHORTCUT = 5\n\nexport default function KeyboardShortcutEditor({\n  shortcut,\n  onShortcutChange,\n  hideTitle = false,\n  className = '',\n  keySize = 60,\n  editButtonText = 'Change Shortcut',\n  confirmButtonText = 'Yes',\n  showConfirmButton = false,\n  onConfirm,\n  editModeTitle = 'Press a key to add it to the shortcut, press it again to remove it',\n  viewModeTitle,\n  minHeight = 84,\n  editButtonClassName = '',\n  confirmButtonClassName = '',\n}: KeyboardShortcutEditorProps) {\n  const shortcutKeys = shortcut.keys\n  const platform = usePlatform()\n  const editorKey = useMemo(\n    () => `keyboard-shortcut-editor:${shortcut.id}`,\n    [shortcut.id],\n  )\n  const { start, stop, activeEditor } = useShortcutEditingStore()\n\n  const cleanupRef = useRef<(() => void) | null>(null)\n  const keyStateRef = useRef<KeyState>(new KeyState(shortcutKeys))\n  const [pressedKeys, setPressedKeys] = useState<string[]>([])\n  const [isEditing, setIsEditing] = useState(false)\n  const [newShortcut, setNewShortcut] = useState<KeyName[]>([])\n  const [validationError, setValidationError] = useState<string>('')\n  const [temporaryError, setTemporaryError] = useState<string>('')\n  const { setIsShortcutEnabled } = useAudioStore()\n  const errorTimeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n  const handleKeyEvent = useCallback(\n    (event: any) => {\n      // Update the key state\n      keyStateRef.current.update(event)\n\n      // Get the current pressed keys and update state\n      const currentPressedKeys = keyStateRef.current.getPressedKeys()\n      setPressedKeys(currentPressedKeys)\n\n      if (isEditing) {\n        // In edit mode, handle adding/removing keys\n        if (event.type === 'keydown') {\n          const normalizedKey = keyNameMap[event.key] || event.key.toLowerCase()\n          if (normalizedKey === 'fn_fast') {\n            return\n          }\n\n          let updatedShortcut: KeyName[]\n          if (!newShortcut.includes(normalizedKey)) {\n            // Check if we're at the limit before adding\n            if (newShortcut.length >= MAX_KEYS_PER_SHORTCUT) {\n              // Clear any existing timeout\n              if (errorTimeoutRef.current) {\n                clearTimeout(errorTimeoutRef.current)\n              }\n\n              // Show temporary error\n              setTemporaryError(`Maximum ${MAX_KEYS_PER_SHORTCUT} keys allowed`)\n\n              // Clear temporary error after 2 seconds\n              errorTimeoutRef.current = setTimeout(() => {\n                setTemporaryError('')\n                errorTimeoutRef.current = null\n              }, 2000)\n\n              return\n            }\n            updatedShortcut = [...newShortcut, normalizedKey]\n          } else {\n            updatedShortcut = newShortcut.filter(key => key !== normalizedKey)\n          }\n\n          // Check for reserved combinations\n          const reservedCheck = isReservedCombination(updatedShortcut, platform)\n          if (reservedCheck.isReserved) {\n            setValidationError(\n              reservedCheck.reason || 'This key combination is reserved',\n            )\n          } else {\n            setValidationError('')\n          }\n\n          setNewShortcut(updatedShortcut)\n        }\n      }\n    },\n    [isEditing, newShortcut, platform],\n  )\n\n  useEffect(() => {\n    // Update key state when shortcut changes\n    keyStateRef.current.updateShortcut(shortcutKeys)\n  }, [shortcut, shortcutKeys])\n\n  useEffect(() => {\n    // Capture the current keyState ref value for cleanup\n    const currentKeyState = keyStateRef.current\n\n    // Listen for key events and store cleanup function\n    try {\n      const cleanup = window.api.onKeyEvent(handleKeyEvent)\n      cleanupRef.current = cleanup\n    } catch (error) {\n      console.error('Failed to set up key event handler:', error)\n    }\n\n    // Clean up when component unmounts or editing changes\n    return () => {\n      if (cleanupRef.current) {\n        try {\n          cleanupRef.current()\n        } catch (error) {\n          console.error('Error during cleanup:', error)\n        }\n      }\n      // Clear the key state when unmounting using captured ref value\n      if (currentKeyState) {\n        currentKeyState.clear()\n      }\n    }\n  }, [handleKeyEvent, isEditing])\n\n  useEffect(() => {\n    return () => {\n      if (isEditing) {\n        window.api.send(\n          'electron-store-set',\n          'settings.isShortcutGloballyEnabled',\n          true,\n        )\n        stop(editorKey)\n      }\n      // Clean up any pending error timeout\n      if (errorTimeoutRef.current) {\n        clearTimeout(errorTimeoutRef.current)\n      }\n    }\n  }, [isEditing, stop, editorKey])\n\n  const handleStartEditing = () => {\n    if (!start(editorKey)) {\n      return\n    }\n    // Disable the shortcut in the main process via IPC\n    window.api.send(\n      'electron-store-set',\n      'settings.isShortcutGloballyEnabled',\n      false,\n    )\n    setIsShortcutEnabled(false)\n    setIsEditing(true)\n    setNewShortcut([])\n    setValidationError('')\n    setTemporaryError('')\n  }\n\n  const handleCancel = () => {\n    window.api.send(\n      'electron-store-set',\n      'settings.isShortcutGloballyEnabled',\n      true,\n    )\n    setIsShortcutEnabled(true)\n    setIsEditing(false)\n    setNewShortcut([])\n    setTemporaryError('')\n    stop(editorKey)\n  }\n\n  const handleSave = () => {\n    if (newShortcut.length === 0) {\n      // Don't save empty shortcuts\n      return\n    }\n    keyStateRef.current.updateShortcut(newShortcut)\n    onShortcutChange(shortcut.id, newShortcut)\n    setIsEditing(false)\n    setIsShortcutEnabled(true)\n    window.api.send(\n      'electron-store-set',\n      'settings.isShortcutGloballyEnabled',\n      true,\n    )\n    stop(editorKey)\n  }\n\n  function isDisplayKeyPressed(displayKey: string, pressed: string[]): boolean {\n    return pressed.includes(displayKey.toLowerCase())\n  }\n\n  return (\n    <div className={`bg-white rounded-lg ${className}`}>\n      {isEditing ? (\n        <>\n          {!hideTitle && (\n            <div className=\"text-lg font-medium mb-6 text-center\">\n              {editModeTitle}\n            </div>\n          )}\n          <div\n            className=\"flex justify-center items-center mb-4 w-full bg-neutral-100 py-3 rounded-lg gap-2\"\n            style={{ minHeight }}\n          >\n            {newShortcut.map((keyboardKey, index) => (\n              <KeyboardKey\n                key={index}\n                keyboardKey={keyboardKey}\n                className=\"bg-white border-2 border-neutral-300\"\n                style={{\n                  width: `${keySize}px`,\n                  height: `${keySize}px`,\n                }}\n              />\n            ))}\n            {newShortcut.length === 0 && (\n              <div className=\"text-gray-400 text-sm\">\n                Press keys to add them (max {MAX_KEYS_PER_SHORTCUT} keys)\n              </div>\n            )}\n          </div>\n          {(validationError || temporaryError) && (\n            <div className=\"text-red-500 text-sm text-center mb-2\">\n              {temporaryError || validationError}\n            </div>\n          )}\n          <div className=\"flex gap-2 justify-end w-full mt-1\">\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              type=\"button\"\n              onClick={handleCancel}\n            >\n              Cancel\n            </Button>\n            <Button\n              size=\"sm\"\n              type=\"button\"\n              onClick={handleSave}\n              disabled={newShortcut.length === 0 || !!validationError}\n            >\n              Save\n            </Button>\n          </div>\n        </>\n      ) : (\n        <>\n          {viewModeTitle && !hideTitle && (\n            <div className=\"text-lg font-medium mb-6 text-center\">\n              {viewModeTitle}\n            </div>\n          )}\n          <div\n            className=\"flex justify-center items-center mb-4 w-full bg-neutral-100 py-3 rounded-lg gap-2\"\n            style={{ minHeight }}\n          >\n            {shortcutKeys.map((keyboardKey, index) => (\n              <KeyboardKey\n                key={index}\n                keyboardKey={keyboardKey}\n                className={`${isDisplayKeyPressed(String(keyboardKey), pressedKeys) ? 'bg-purple-50 border-2 border-purple-200' : 'bg-white border-2 border-neutral-300'}`}\n                style={{\n                  width: `${keySize}px`,\n                  height: `${keySize}px`,\n                }}\n              />\n            ))}\n          </div>\n          <div className=\"flex justify-end gap-2 w-full mt-1\">\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              type=\"button\"\n              onClick={handleStartEditing}\n              className={editButtonClassName}\n              disabled={activeEditor !== null && activeEditor !== editorKey}\n            >\n              {editButtonText}\n            </Button>\n            {showConfirmButton && onConfirm && (\n              <Button\n                size=\"sm\"\n                type=\"button\"\n                onClick={onConfirm}\n                className={confirmButtonClassName}\n                disabled={activeEditor !== null && activeEditor !== editorKey}\n              >\n                {confirmButtonText}\n              </Button>\n            )}\n          </div>\n        </>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/ui/microphone-selector.tsx",
    "content": "import { Button } from '@/app/components/ui/button'\nimport { useEffect, useState } from 'react'\nimport {\n  getAvailableMicrophones,\n  microphoneToRender,\n  Microphone,\n} from '@/app/media/microphone'\nimport {\n  Dialog,\n  DialogContent,\n  DialogTrigger,\n  DialogClose,\n  DialogTitle,\n  DialogDescription,\n} from '@/app/components/ui/dialog'\n\ninterface MicrophoneSelectorProps {\n  selectedDeviceId?: string\n  selectedMicrophoneName?: string\n  onSelectionChange: (deviceId: string, name: string) => void\n  triggerButtonText?: string\n  triggerButtonVariant?:\n    | 'default'\n    | 'outline'\n    | 'secondary'\n    | 'ghost'\n    | 'link'\n    | 'destructive'\n  triggerButtonClassName?: string\n}\n\nexport function MicrophoneSelector({\n  selectedDeviceId,\n  selectedMicrophoneName,\n  onSelectionChange,\n  triggerButtonText = 'Select Microphone',\n  triggerButtonVariant = 'outline',\n  triggerButtonClassName = '',\n}: MicrophoneSelectorProps) {\n  const [availableMicrophones, setAvailableMicrophones] = useState<\n    Array<Microphone>\n  >([])\n  const [tempSelectedMicrophone, setTempSelectedMicrophone] = useState<string>(\n    selectedDeviceId || 'default',\n  )\n  const [isOpen, setIsOpen] = useState(false)\n\n  useEffect(() => {\n    const loadMicrophones = async () => {\n      try {\n        const mics = await getAvailableMicrophones()\n        setAvailableMicrophones(mics)\n      } catch (error) {\n        console.error('Failed to load microphones:', error)\n      }\n    }\n    // Only load microphones when the dialog is opened\n    if (isOpen) {\n      loadMicrophones()\n    }\n  }, [isOpen])\n\n  useEffect(() => {\n    setTempSelectedMicrophone(selectedDeviceId || 'default')\n  }, [selectedDeviceId])\n\n  const handleMicrophoneSelect = (deviceId: string) => {\n    setTempSelectedMicrophone(deviceId)\n  }\n\n  const handleDialogClose = () => {\n    if (tempSelectedMicrophone !== selectedDeviceId) {\n      const selectedMic = availableMicrophones.find(\n        mic => mic.deviceId === tempSelectedMicrophone,\n      )\n      const selectedMicName = selectedMic\n        ? microphoneToRender(selectedMic).title\n        : 'Auto-detect'\n      onSelectionChange(tempSelectedMicrophone, selectedMicName)\n    }\n    setIsOpen(false)\n  }\n\n  // Use saved microphone name if available, otherwise fallback to looking it up\n  const selectedMicrophoneDisplay =\n    selectedMicrophoneName ||\n    (() => {\n      const foundMicrophone = availableMicrophones.find(\n        mic => mic.deviceId === selectedDeviceId,\n      )\n      return foundMicrophone\n        ? microphoneToRender(foundMicrophone).title\n        : 'Auto-detect'\n    })()\n\n  return (\n    <Dialog open={isOpen} onOpenChange={setIsOpen}>\n      <DialogTrigger asChild>\n        <Button\n          variant={triggerButtonVariant}\n          className={triggerButtonClassName}\n          type=\"button\"\n        >\n          {triggerButtonText === 'Select Microphone' &&\n          (selectedMicrophoneName || selectedDeviceId)\n            ? selectedMicrophoneDisplay\n            : triggerButtonText}\n        </Button>\n      </DialogTrigger>\n      <DialogContent\n        className=\"!border-0 shadow-lg p-0\"\n        showCloseButton={false}\n      >\n        <DialogTitle className=\"sr-only\">Select Microphone</DialogTitle>\n        <DialogDescription className=\"sr-only\">\n          Choose a microphone from the list below to use for voice input\n        </DialogDescription>\n        <div className=\"max-h-[60vh] overflow-y-auto space-y-3 pl-8 pr-6 pt-8\">\n          {availableMicrophones.map(mic => {\n            const { title, description } = microphoneToRender(mic)\n            return (\n              <div\n                key={mic.deviceId}\n                className={`p-6 rounded-md cursor-pointer transition-colors max-w-full overflow-hidden ${\n                  tempSelectedMicrophone === mic.deviceId\n                    ? 'bg-purple-50 border-2 border-purple-100'\n                    : 'bg-neutral-100 border-2 border-neutral-100 hover:bg-neutral-200'\n                }`}\n                onClick={() => handleMicrophoneSelect(mic.deviceId)}\n                style={{ minWidth: 0 }}\n              >\n                <div\n                  className=\"font-medium text-base truncate\"\n                  style={{ maxWidth: '100%' }}\n                >\n                  {title}\n                </div>\n                {description && (\n                  <div\n                    className=\"text-sm text-muted-foreground text-wrap mt-2\"\n                    style={{ maxWidth: '100%' }}\n                  >\n                    {description}\n                  </div>\n                )}\n              </div>\n            )\n          })}\n        </div>\n        <div className=\"flex justify-end px-8 pb-8 pt-6\">\n          <DialogClose asChild>\n            <Button className=\"w-32\" type=\"button\" onClick={handleDialogClose}>\n              Save and close\n            </Button>\n          </DialogClose>\n        </div>\n      </DialogContent>\n    </Dialog>\n  )\n}\n"
  },
  {
    "path": "app/components/ui/multi-shortcut-editor.tsx",
    "content": "import { useCallback, useEffect, useRef, useState, useMemo } from 'react'\nimport KeyboardKey from '@/app/components/ui/keyboard-key'\nimport { ShortcutError } from '@/app/utils/keyboard'\nimport { keyNameMap } from '@/lib/types/keyboard'\nimport { ItoMode } from '@/app/generated/ito_pb'\nimport { useSettingsStore } from '@/app/store/useSettingsStore'\nimport { Check, Pencil } from '@mynaui/icons-react'\nimport { cx } from 'class-variance-authority'\nimport { KeyName } from '@/lib/types/keyboard'\nimport { useShortcutEditingStore } from '@/app/store/useShortcutEditingStore'\n\nexport interface KeyboardShortcutConfig {\n  id: string\n  keys: KeyName[]\n  mode: ItoMode\n}\n\ntype Props = {\n  shortcuts: KeyboardShortcutConfig[] // persisted rows\n  mode: ItoMode\n  className?: string\n  keySize?: number\n  maxShortcutsPerMode?: number\n}\n\nconst MAX_KEYS_PER_SHORTCUT = 5\n\nexport default function MultiShortcutEditor({\n  shortcuts,\n  mode,\n  className = '',\n  maxShortcutsPerMode = 5,\n}: Props) {\n  const {\n    createKeyboardShortcut,\n    removeKeyboardShortcut,\n    updateKeyboardShortcut,\n  } = useSettingsStore()\n\n  // global editing lock\n  const editorKey = useMemo(() => `multi-shortcut-editor:${mode}`, [mode])\n  const { start, stop, activeEditor } = useShortcutEditingStore()\n\n  const rows = useMemo(\n    () => (mode == null ? shortcuts : shortcuts.filter(s => s.mode === mode)),\n    [shortcuts, mode],\n  )\n  const isAtLimit = rows.length >= maxShortcutsPerMode\n  const isMinimum = rows.length <= 1\n\n  // editing state\n  const [editingId, setEditingId] = useState<string | null>(null) // existing row id or \"__new__\"\n  const [draftKeys, setDraftKeys] = useState<KeyName[]>([])\n  const [error, setError] = useState<string>('')\n  const [temporaryError, setTemporaryError] = useState<string>('')\n\n  const cleanupRef = useRef<(() => void) | null>(null)\n  const errorTimeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n  const beginEditExisting = (row: KeyboardShortcutConfig) => {\n    if (!start(editorKey)) {\n      setError('Finish editing the other shortcut set first.')\n      return\n    }\n    setEditingId(row.id)\n    setDraftKeys([])\n    setError('')\n    setTemporaryError('')\n\n    window.api.send(\n      'electron-store-set',\n      'settings.isShortcutGloballyEnabled',\n      false,\n    )\n  }\n\n  const getErrorMessage = (error: ShortcutError, message?: string) => {\n    switch (error) {\n      case 'duplicate-key-same-mode':\n        return 'This key combination is already in use for this mode.'\n      case 'duplicate-key-diff-mode':\n        return 'This key combination is already in use for a different mode.'\n      case 'not-found':\n        return 'The specified shortcut was not found.'\n      case 'reserved-combination':\n        return message || 'This key combination is reserved and cannot be used.'\n      default:\n        return 'An unknown error occurred.'\n    }\n  }\n\n  const addNew = () => {\n    const result = createKeyboardShortcut(mode)\n    if (!result.success && result.error) {\n      setError(getErrorMessage(result.error, result.errorMessage))\n      return\n    }\n  }\n\n  const stopEdit = () => {\n    setEditingId(null)\n    setDraftKeys([])\n    setError('')\n    setTemporaryError('')\n\n    // Clear any pending error timeout\n    if (errorTimeoutRef.current) {\n      clearTimeout(errorTimeoutRef.current)\n      errorTimeoutRef.current = null\n    }\n\n    window.api.send(\n      'electron-store-set',\n      'settings.isShortcutGloballyEnabled',\n      true,\n    )\n    stop(editorKey)\n  }\n\n  const saveEdit = async (original: KeyboardShortcutConfig) => {\n    if (!draftKeys.length) return\n\n    // update existing\n    const result = await updateKeyboardShortcut(original.id, draftKeys)\n    if (!result.success && result.error) {\n      setError(getErrorMessage(result.error, result.errorMessage))\n      return\n    }\n\n    stopEdit()\n  }\n\n  // capture keys (no normalization/cleanup here by request)\n  const handleKeyEvent = useCallback(\n    (event: any) => {\n      if (!editingId || event.type !== 'keydown') return\n      const key = keyNameMap[event.key] || event.key.toLowerCase()\n      if (key === 'fn_fast') return\n\n      setDraftKeys(prev => {\n        // If key exists, remove it\n        if (prev.includes(key)) {\n          return prev.filter(k => k !== key)\n        }\n\n        // If at max keys, show temporary error and don't add\n        if (prev.length >= MAX_KEYS_PER_SHORTCUT) {\n          // Clear any existing timeout\n          if (errorTimeoutRef.current) {\n            clearTimeout(errorTimeoutRef.current)\n          }\n\n          // Show temporary error\n          setTemporaryError(`Maximum ${MAX_KEYS_PER_SHORTCUT} keys allowed`)\n\n          // Clear temporary error after 2 seconds\n          errorTimeoutRef.current = setTimeout(() => {\n            setTemporaryError('')\n            errorTimeoutRef.current = null\n          }, 2000)\n\n          return prev\n        }\n\n        // Add the new key and clear any errors\n        setError('')\n        setTemporaryError('')\n        return [...prev, key]\n      })\n    },\n    [editingId],\n  )\n\n  useEffect(() => {\n    if (!editingId) return\n\n    cleanupRef.current = window.api.onKeyEvent(handleKeyEvent)\n\n    return () => {\n      cleanupRef.current?.()\n    }\n  }, [handleKeyEvent, editingId])\n\n  // Ensure lock is released and global shortcuts re-enabled on unmount\n  useEffect(() => {\n    return () => {\n      if (editingId) {\n        window.api.send(\n          'electron-store-set',\n          'settings.isShortcutGloballyEnabled',\n          true,\n        )\n        stop(editorKey)\n      }\n      // Clean up any pending error timeout\n      if (errorTimeoutRef.current) {\n        clearTimeout(errorTimeoutRef.current)\n      }\n    }\n  }, [editingId, stop, editorKey])\n\n  const base =\n    'inline-flex items-center justify-center rounded-xl border border-neutral-300 ' +\n    'px-3 py-1.5 text-neutral-700 hover:bg-neutral-50 h-9 min-w-[48px] border-0'\n\n  const isLockedByOther = activeEditor !== null && activeEditor !== editorKey\n\n  return (\n    <div className={cx('w-82', className)}>\n      {rows.map(row => {\n        const isEditing = editingId === row.id\n        const displayKeys = isEditing ? draftKeys : row.keys\n\n        return (\n          <div\n            key={row.id}\n            className=\"mb-1 rounded-lg border border-neutral-200 bg-white p-1\"\n          >\n            <div className=\"flex items-center justify-between\">\n              <div className=\"flex items-center justify-between gap-1\">\n                {displayKeys.length ? (\n                  <>\n                    {displayKeys.map((k, idx) => (\n                      <KeyboardKey key={idx} keyboardKey={k} variant=\"inline\" />\n                    ))}\n                    {isEditing &&\n                      displayKeys.length < MAX_KEYS_PER_SHORTCUT && (\n                        <span className=\"text-xs text-neutral-400 ml-2\">\n                          ({MAX_KEYS_PER_SHORTCUT - displayKeys.length} more\n                          allowed)\n                        </span>\n                      )}\n                  </>\n                ) : (\n                  <span className=\"text-neutral-400\">\n                    {isEditing\n                      ? `Press keys to add (max ${MAX_KEYS_PER_SHORTCUT})`\n                      : `No keys set`}\n                  </span>\n                )}\n              </div>\n\n              <div className=\"flex items-center gap-2\">\n                {editingId === row.id ? (\n                  <button\n                    type=\"button\"\n                    onClick={() => saveEdit(row)}\n                    className={base}\n                  >\n                    <Check className=\"h-4 w-4\" />\n                  </button>\n                ) : (\n                  <button\n                    type=\"button\"\n                    onClick={() => beginEditExisting(row)}\n                    className={\n                      base + ' disabled:opacity-50 disabled:cursor-not-allowed'\n                    }\n                    disabled={isLockedByOther}\n                  >\n                    <Pencil className=\"h-4 w-4\" />\n                  </button>\n                )}\n              </div>\n            </div>\n            {editingId === row.id && (error || temporaryError) && (\n              <div className=\"mt-1 text-xs text-red-500\">\n                {temporaryError || error}\n              </div>\n            )}\n          </div>\n        )\n      })}\n\n      <div className=\"flex justify-end\">\n        <button\n          type=\"button\"\n          onClick={() => {\n            const lastRow = rows.at(-1)\n            if (lastRow) {\n              removeKeyboardShortcut(lastRow.id)\n            }\n          }}\n          hidden={isMinimum}\n          className=\"ml-auto text-red-400 hover:underline text-sm disabled:opacity-50 disabled:cursor-not-allowed\"\n          disabled={isLockedByOther}\n        >\n          Delete\n        </button>\n      </div>\n\n      {/* Add new */}\n      <div className=\"mt-2 flex justify-end\">\n        <button\n          type=\"button\"\n          onClick={() => {\n            if (isLockedByOther) return\n            addNew()\n          }}\n          hidden={isAtLimit}\n          className=\"rounded-md border border-neutral-300 py-1 px-2 text-md text-neutral-800 disabled:opacity-50 hover:bg-neutral-50 disabled:cursor-not-allowed\"\n          disabled={isLockedByOther}\n        >\n          Add another\n        </button>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/ui/nav-item.tsx",
    "content": "import { ReactNode } from 'react'\nimport { Tooltip, TooltipTrigger, TooltipContent } from './tooltip'\n\ninterface NavItemProps {\n  icon: ReactNode\n  label: string\n  isActive?: boolean\n  showText: boolean\n  onClick?: () => void\n}\n\nexport function NavItem({\n  icon,\n  label,\n  isActive = false,\n  showText,\n  onClick,\n}: NavItemProps) {\n  const navContent = (\n    <div\n      className={`flex items-center px-3 py-3 rounded cursor-pointer ${\n        isActive ? 'bg-slate-200 font-medium' : 'hover:bg-slate-200'\n      }`}\n      onClick={onClick}\n    >\n      <div className=\"w-6 flex items-center justify-center\">{icon}</div>\n      <span\n        className={`transition-opacity duration-100 ${\n          showText ? 'opacity-100' : 'opacity-0'\n        } ${showText ? 'ml-2' : 'w-0 overflow-hidden'}`}\n      >\n        {label}\n      </span>\n    </div>\n  )\n\n  if (!showText) {\n    return (\n      <Tooltip>\n        <TooltipTrigger asChild>{navContent}</TooltipTrigger>\n        <TooltipContent side=\"right\" sideOffset={2} className=\"text-sm\">\n          {label}\n        </TooltipContent>\n      </Tooltip>\n    )\n  }\n\n  return navContent\n}\n"
  },
  {
    "path": "app/components/ui/note.tsx",
    "content": "import React from 'react'\nimport { Pencil, Copy, Trash, Dots } from '@mynaui/icons-react'\nimport type { Note } from '../../store/useNotesStore'\n\ninterface NoteProps {\n  note: Note\n  index: number\n  showDropdown: number | null\n  onEdit: (noteId: string) => void\n  onToggleDropdown: (index: number, e: React.MouseEvent) => void\n  onCopyToClipboard: (content: string) => void\n  onDeleteNote: (noteId: string) => void\n  formatDate: (date: Date) => string\n  formatTime: (date: Date) => string\n  truncateContent: (content: string, maxLength?: number) => string\n  searchQuery?: string\n}\n\n// Function to highlight matching text\nfunction highlightText(text: string, searchQuery: string): React.ReactElement {\n  if (!searchQuery.trim()) {\n    return <>{text}</>\n  }\n\n  const regex = new RegExp(\n    `(${searchQuery.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')})`,\n    'gi',\n  )\n  const parts = text.split(regex)\n\n  return (\n    <>\n      {parts.map((part, index) => {\n        if (regex.test(part)) {\n          return (\n            <span key={index} className=\"bg-yellow-200 font-medium\">\n              {part}\n            </span>\n          )\n        }\n        return part\n      })}\n    </>\n  )\n}\n\nexport function Note({\n  note,\n  index,\n  showDropdown,\n  onEdit,\n  onToggleDropdown,\n  onCopyToClipboard,\n  onDeleteNote,\n  formatDate,\n  formatTime,\n  truncateContent,\n  searchQuery,\n}: NoteProps) {\n  // Determine what content to display\n  const displayContent = searchQuery\n    ? note.content\n    : truncateContent(note.content)\n\n  return (\n    <div\n      key={note.id}\n      className=\"bg-white rounded-lg border border-gray-200 p-4 shadow-sm hover:shadow-md group relative\"\n    >\n      {/* Hover Icons */}\n      <div className=\"absolute top-2 right-2 opacity-0 group-hover:shadow-sm group-hover:opacity-100 transition-opacity duration-200 flex items-center rounded-md\">\n        <button\n          onClick={e => {\n            e.stopPropagation()\n            onEdit(note.id)\n          }}\n          className=\"p-1.5 hover:bg-gray-100 transition-colors border-r border-neutral-200 rounded-l-md cursor-pointer \"\n        >\n          <Pencil className=\"w-4 h-4 text-neutral-500\" />\n        </button>\n        <div className=\"relative\">\n          <button\n            onClick={e => onToggleDropdown(index, e)}\n            className=\"p-1.5 hover:bg-gray-100 transition-colors rounded-r-md cursor-pointer\"\n          >\n            <Dots className=\"w-4 h-4 text-neutral-800\" />\n          </button>\n\n          {/* Dropdown Menu */}\n          {showDropdown === index && (\n            <div className=\"absolute top-full right-0 mt-1 w-48 bg-white border border-gray-200 rounded-lg shadow-lg z-10\">\n              <button\n                onClick={e => {\n                  e.stopPropagation()\n                  onCopyToClipboard(note.content)\n                }}\n                className=\"w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2 rounded-t-lg cursor-pointer\"\n              >\n                <Copy className=\"w-4 h-4\" />\n                Copy to clipboard\n              </button>\n              <button\n                onClick={e => {\n                  e.stopPropagation()\n                  onDeleteNote(note.id)\n                }}\n                className=\"w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-2 rounded-b-lg cursor-pointer\"\n              >\n                <Trash className=\"w-4 h-4\" />\n                Delete note\n              </button>\n            </div>\n          )}\n        </div>\n      </div>\n\n      <div className=\"flex flex-col\">\n        <div className=\"mb-4 pr-16\">\n          <div className=\"text-gray-900 font-normal text-sm leading-relaxed break-words\">\n            {searchQuery\n              ? highlightText(displayContent, searchQuery)\n              : displayContent}\n          </div>\n        </div>\n        <div className=\"flex items-center justify-between text-gray-400 text-xs mt-auto\">\n          <span>{formatDate(new Date(note.created_at))}</span>\n          <span>{formatTime(new Date(note.created_at))}</span>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/ui/spinner.tsx",
    "content": "import React from 'react'\nimport { cn } from '@/lib/utils'\nimport { VariantProps, cva } from 'class-variance-authority'\nimport { Loader2 } from 'lucide-react'\n\nconst spinnerVariants = cva('flex-col items-center justify-center', {\n  variants: {\n    show: {\n      true: 'flex',\n      false: 'hidden',\n    },\n  },\n  defaultVariants: {\n    show: true,\n  },\n})\n\nconst loaderVariants = cva('animate-spin text-primary', {\n  variants: {\n    size: {\n      small: 'size-6',\n      medium: 'size-8',\n      large: 'size-12',\n    },\n  },\n  defaultVariants: {\n    size: 'medium',\n  },\n})\n\ninterface SpinnerContentProps\n  extends VariantProps<typeof spinnerVariants>,\n    VariantProps<typeof loaderVariants> {\n  className?: string\n  children?: React.ReactNode\n}\n\nexport function Spinner({\n  size,\n  show,\n  children,\n  className,\n}: SpinnerContentProps) {\n  return (\n    <span className={spinnerVariants({ show })}>\n      <Loader2 className={cn(loaderVariants({ size }), className)} />\n      {children}\n    </span>\n  )\n}\n"
  },
  {
    "path": "app/components/ui/status-indicator.tsx",
    "content": "import { useEffect, useState } from 'react'\nimport { Check, X } from '@mynaui/icons-react'\n\ninterface StatusIndicatorProps {\n  status: 'success' | 'error' | null\n  onHide?: () => void\n  duration?: number\n  successMessage?: string\n  errorMessage?: string\n}\n\nexport function StatusIndicator({\n  status,\n  onHide,\n  duration = 2000,\n  successMessage = 'Operation completed successfully',\n  errorMessage = 'Operation failed',\n}: StatusIndicatorProps) {\n  const [isExiting, setIsExiting] = useState(false)\n\n  useEffect(() => {\n    if (status) {\n      setIsExiting(false)\n      const timer = setTimeout(() => {\n        setIsExiting(true)\n        setTimeout(() => {\n          onHide?.()\n        }, 300) // Wait for exit animation\n      }, duration)\n\n      return () => clearTimeout(timer)\n    }\n\n    return undefined\n  }, [status, duration, onHide])\n\n  if (!status) return null\n\n  return (\n    <div\n      className={`fixed bottom-6 right-6 z-500 transition-all duration-300 ease-out ${\n        isExiting\n          ? 'translate-y-[-20px] opacity-0'\n          : 'translate-y-0 opacity-100 animate-slide-up'\n      }`}\n    >\n      <div className=\"px-4 py-3 rounded-lg shadow-lg flex items-center gap-2 bg-black text-white\">\n        {status === 'success' ? (\n          <>\n            <Check className=\"w-4 h-4 text-green-400\" />\n            <span className=\"font-medium\">{successMessage}</span>\n          </>\n        ) : (\n          <>\n            <X className=\"w-4 h-4 text-red-400\" />\n            <span className=\"font-medium\">{errorMessage}</span>\n          </>\n        )}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/ui/switch.tsx",
    "content": "import * as React from 'react'\nimport * as SwitchPrimitive from '@radix-ui/react-switch'\n\nimport { cn } from '@/lib/utils'\n\nfunction Switch({\n  className,\n  ...props\n}: React.ComponentProps<typeof SwitchPrimitive.Root>) {\n  return (\n    <SwitchPrimitive.Root\n      data-slot=\"switch\"\n      className={cn(\n        'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',\n        className,\n      )}\n      {...props}\n    >\n      <SwitchPrimitive.Thumb\n        data-slot=\"switch-thumb\"\n        className={cn(\n          'bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0',\n        )}\n      />\n    </SwitchPrimitive.Root>\n  )\n}\n\nexport { Switch }\n"
  },
  {
    "path": "app/components/ui/tip.tsx",
    "content": "import { InfoCircleSolid } from '@mynaui/icons-react'\nimport { cx } from 'class-variance-authority'\n\nexport function Tip({\n  tipText,\n  className,\n}: {\n  tipText: string\n  className?: string\n}) {\n  const baseClass = 'flex gap-2 items-center '\n  return (\n    <div className={cx(baseClass, className)}>\n      <span className=\"align-middle inline-flex\">\n        <InfoCircleSolid className=\"fill-blue-400 h-6 w-6\" />\n      </span>\n      <span>\n        <span className=\"font-semibold\">Tip:</span> {tipText}\n      </span>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/ui/tooltip.tsx",
    "content": "import * as React from 'react'\nimport * as TooltipPrimitive from '@radix-ui/react-tooltip'\n\nimport { cn } from '@/lib/utils'\n\nfunction TooltipProvider({\n  delayDuration = 0,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {\n  return (\n    <TooltipPrimitive.Provider\n      data-slot=\"tooltip-provider\"\n      delayDuration={delayDuration}\n      {...props}\n    />\n  )\n}\n\nfunction Tooltip({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Root>) {\n  return (\n    <TooltipProvider>\n      <TooltipPrimitive.Root data-slot=\"tooltip\" {...props} />\n    </TooltipProvider>\n  )\n}\n\nfunction TooltipTrigger({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {\n  return <TooltipPrimitive.Trigger data-slot=\"tooltip-trigger\" {...props} />\n}\n\nfunction TooltipContent({\n  className,\n  sideOffset = 0,\n  children,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Content>) {\n  return (\n    <TooltipPrimitive.Portal>\n      <TooltipPrimitive.Content\n        data-slot=\"tooltip-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          'bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </TooltipPrimitive.Content>\n    </TooltipPrimitive.Portal>\n  )\n}\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }\n"
  },
  {
    "path": "app/components/welcome/WelcomeKit.tsx",
    "content": "import CreateAccountContent from './contents/CreateAccountContent'\nimport SignInContent from './contents/SignInContent'\nimport ReferralContent from './contents/ReferralContent'\nimport DataControlContent from './contents/DataControlContent'\nimport PermissionsContent from './contents/PermissionsContent'\nimport MicrophoneTestContent from './contents/MicrophoneTestContent'\nimport KeyboardTestContext from './contents/KeyboardTestContext'\nimport GoodToGoContent from './contents/GoodToGoContent'\nimport AnyAppContent from './contents/AnyAppContent'\nimport TryItOutContent from './contents/TryItOutContent'\nimport { useEffect } from 'react'\nimport './styles.css'\nimport { usePermissionsStore } from '../../store/usePermissionsStore'\nimport { useOnboardingStore } from '@/app/store/useOnboardingStore'\nimport { useAuthStore } from '@/app/store/useAuthStore'\nimport IntroducingIntelligentModeContent from './contents/IntroducingIntelligentModeContent'\n\nexport default function WelcomeKit() {\n  const { onboardingStep } = useOnboardingStore()\n  const { isAuthenticated, user } = useAuthStore()\n\n  const onboardingStepOrder = [\n    CreateAccountContent,\n    ReferralContent,\n    DataControlContent,\n    PermissionsContent,\n    MicrophoneTestContent,\n    KeyboardTestContext,\n    GoodToGoContent,\n    IntroducingIntelligentModeContent,\n    AnyAppContent,\n    TryItOutContent,\n  ]\n\n  const { setAccessibilityEnabled, setMicrophoneEnabled } =\n    usePermissionsStore()\n\n  useEffect(() => {\n    window.api\n      .invoke('check-accessibility-permission', false)\n      .then((enabled: boolean) => {\n        setAccessibilityEnabled(enabled)\n      })\n\n    window.api\n      .invoke('check-microphone-permission', false)\n      .then((enabled: boolean) => {\n        setMicrophoneEnabled(enabled)\n      })\n  }, [setAccessibilityEnabled, setMicrophoneEnabled])\n\n  // Show signin/signup based on whether user has previous auth data\n  if (!isAuthenticated) {\n    if (user) {\n      // Returning user who needs to sign back in\n      return <SignInContent />\n    } else {\n      // New user who needs to create an account\n      return <CreateAccountContent />\n    }\n  }\n\n  const CurrentComponent = onboardingStepOrder[onboardingStep]\n\n  return (\n    <div className=\"w-full h-full bg-background\">\n      {CurrentComponent ? <CurrentComponent /> : null}\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/welcome/contents/AnyAppContent.tsx",
    "content": "import { Button } from '@/app/components/ui/button'\nimport { useOnboardingStore } from '@/app/store/useOnboardingStore'\nimport { AppOrbitImage } from '@/app/components/ui/app-orbit-image'\n\nexport default function AnyAppContent() {\n  const { incrementOnboardingStep, decrementOnboardingStep } =\n    useOnboardingStore()\n\n  return (\n    <div className=\"flex flex-row h-full w-full bg-background\">\n      <div className=\"flex flex-col w-[45%] justify-center items-start px-24\">\n        <div className=\"flex flex-col h-full min-h-[400px] justify-between py-12\">\n          <div className=\"mt-8\">\n            <button\n              className=\"mb-4 text-sm text-muted-foreground hover:underline\"\n              type=\"button\"\n              onClick={decrementOnboardingStep}\n            >\n              &lt; Back\n            </button>\n            <h1 className=\"text-3xl mb-4 mt-12\">Ito works in any app.</h1>\n            <p className=\"text-base text-muted-foreground mt-6\">\n              From emails to chats to documents—Ito works in any textbox on your\n              computer.\n            </p>\n          </div>\n          <div className=\"flex flex-col items-start mb-8\">\n            <Button className=\"w-24\" onClick={incrementOnboardingStep}>\n              Continue\n            </Button>\n          </div>\n        </div>\n      </div>\n      <div className=\"flex w-[55%] items-center justify-center bg-gradient-to-b from-purple-50/10 to-purple-100 border-l-2 border-purple-100\">\n        <AppOrbitImage />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/welcome/contents/CheckEmailContent.tsx",
    "content": "import { useEffect, useState } from 'react'\nimport { Button } from '@/app/components/ui/button'\nimport { AppOrbitImage } from '@/app/components/ui/app-orbit-image'\nimport { useAuth } from '../../auth/useAuth'\n\ntype Props = {\n  email: string\n  password: string | null\n  dbUserId: string | null\n  onUseAnotherEmail: () => void\n  onRequireLogin?: () => void\n}\n\nexport default function CheckEmailContent({\n  email,\n  password,\n  dbUserId,\n  onUseAnotherEmail,\n  onRequireLogin = () => {},\n}: Props) {\n  const [seconds, setSeconds] = useState(30)\n  const [isResending, setIsResending] = useState(false)\n  const [pollError, setPollError] = useState<string | null>(null)\n  const [resendError, setResendError] = useState<string | null>(null)\n\n  const { loginWithEmailPassword } = useAuth()\n\n  useEffect(() => {\n    if (seconds <= 0) return\n    const id = setInterval(() => setSeconds(s => (s > 0 ? s - 1 : 0)), 1000)\n    return () => clearInterval(id)\n  }, [seconds])\n\n  const handleResend = async () => {\n    if (seconds > 0 || isResending) return\n    try {\n      setIsResending(true)\n      setResendError(null)\n      let success = true\n      console.log('Resending verification email for', email, dbUserId)\n      const res = await window.api.invoke('auth0-send-verification', {\n        dbUserId,\n      })\n      if (!res?.success) {\n        success = false\n        setResendError(res?.error || 'Failed to resend verification email')\n      } else if (!res?.jobId) {\n        setResendError(\n          'Verification email requested but no job id was returned',\n        )\n      }\n      if (success) setSeconds(30)\n    } finally {\n      setIsResending(false)\n    }\n  }\n\n  // Poll for verification status every 4 seconds\n  useEffect(() => {\n    let mounted = true\n    const poll = async () => {\n      try {\n        console.log('Polling for email verification')\n        const res = await window.api.invoke('auth0-check-email', {\n          email,\n        })\n        if (mounted && res?.success && res.verified) {\n          console.log('Email verified')\n          if (password) {\n            await loginWithEmailPassword(email, password, {\n              skipNavigate: true,\n            })\n          } else {\n            onRequireLogin()\n          }\n        }\n        if (mounted && !res?.success) {\n          setPollError(res?.error || null)\n        }\n      } catch (e: any) {\n        if (mounted) setPollError(e?.message || 'Polling error')\n      }\n    }\n    const id = setInterval(poll, 2000)\n    poll()\n    return () => {\n      mounted = false\n      clearInterval(id)\n    }\n  }, [email, dbUserId, loginWithEmailPassword, onRequireLogin, password])\n\n  return (\n    <div className=\"flex h-full w-full bg-background\">\n      {/* Left content */}\n      <div className=\"flex w-1/2 flex-col justify-center px-16\">\n        <div className=\"mb-8\">\n          <h1 className=\"text-3xl font-semibold text-foreground\">\n            Check your email\n          </h1>\n          <p className=\"mt-2 text-sm text-muted-foreground\">\n            We've sent a message to {email}.\n          </p>\n        </div>\n\n        <ol className=\"mb-6 list-decimal space-y-3 pl-5 text-sm text-foreground\">\n          <li>\n            Open the email and click{' '}\n            <span className=\"font-medium\">Confirm email</span> to activate your\n            account.\n          </li>\n          <li>\n            Once verified, return here - this page will refresh automatically.\n          </li>\n        </ol>\n\n        <div className=\"mb-4\">\n          <Button\n            variant=\"outline\"\n            disabled={seconds > 0 || isResending}\n            onClick={handleResend}\n            className=\"h-10 w-full justify-center\"\n          >\n            {seconds > 0\n              ? `Resend email (${seconds} Sec)`\n              : isResending\n                ? 'Resending…'\n                : 'Resend email'}\n          </Button>\n          {resendError && (\n            <p className=\"mt-2 text-xs text-destructive\">{resendError}</p>\n          )}\n        </div>\n\n        <button\n          className=\"text-sm text-foreground underline\"\n          onClick={onUseAnotherEmail}\n        >\n          Use another email\n        </button>\n\n        <p className=\"mt-6 max-w-sm text-center text-xs text-muted-foreground\">\n          If you don't see it, check your Spam or Promotions folder for a\n          message from support@ito.ai\n        </p>\n        {pollError && (\n          <p className=\"mt-2 text-center text-xs text-muted-foreground\">\n            {pollError}\n          </p>\n        )}\n      </div>\n\n      {/* Right illustration */}\n      <div className=\"flex w-1/2 items-center justify-center border-l border-border bg-muted/20\">\n        <AppOrbitImage />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/welcome/contents/CreateAccountContent.tsx",
    "content": "import { Button } from '@/app/components/ui/button'\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from '@/app/components/ui/dialog'\nimport { useOnboardingStore } from '@/app/store/useOnboardingStore'\nimport EmailSignupContent from './EmailSignupContent'\nimport EmailLoginContent from './EmailLoginContent'\nimport CheckEmailContent from './CheckEmailContent'\nimport ItoIcon from '../../icons/ItoIcon'\nimport UserCog from '@/app/assets/icons/UserCog.svg'\nimport GoogleIcon from '../../icons/GoogleIcon'\nimport AppleIcon from '../../icons/AppleIcon'\nimport GitHubIcon from '../../icons/GitHubIcon'\nimport MicrosoftIcon from '../../icons/MicrosoftIcon'\nimport { useEffect, useRef, useState } from 'react'\nimport { useAuth } from '../../auth/useAuth'\nimport { checkLocalServerHealth } from '@/app/utils/healthCheck'\nimport { useDictionaryStore } from '@/app/store/useDictionaryStore'\nimport { EXTERNAL_LINKS } from '@/lib/constants/external-links'\nimport { isValidEmail } from '@/app/utils/utils'\n\nexport default function CreateAccountContent() {\n  const { incrementOnboardingStep, initializeOnboarding } = useOnboardingStore()\n  const [isServerHealthy, setIsServerHealthy] = useState(true)\n  const [isSelfHostedModalOpen, setIsSelfHostedModalOpen] = useState(false)\n  const [email, setEmail] = useState('')\n  const [emailTouched, setEmailTouched] = useState(false)\n  const isDictInitialized = useRef(false)\n  const [showEmailPassword, setShowEmailPassword] = useState(false)\n  const [showEmailLogin, setShowEmailLogin] = useState(false)\n  const [showCheckEmail, setShowCheckEmail] = useState(false)\n  const [checkEmailDbUserId, setCheckEmailDbUserId] = useState<string | null>(\n    null,\n  )\n  const [isCheckingEmail, setIsCheckingEmail] = useState(false)\n  const [checkError, setCheckError] = useState<string | null>(null)\n\n  const {\n    user,\n    isAuthenticated,\n    loginWithGoogle,\n    loginWithMicrosoft,\n    loginWithApple,\n    loginWithGitHub,\n    loginWithSelfHosted,\n    signupWithEmail,\n  } = useAuth()\n  const userName = user?.name\n\n  const addEntry = useDictionaryStore(state => state.addEntry)\n\n  // If user is authenticated, proceed to next step\n  useEffect(() => {\n    if (isAuthenticated && user) {\n      incrementOnboardingStep()\n    }\n  }, [isAuthenticated, user, incrementOnboardingStep])\n\n  useEffect(() => {\n    if (userName && !isDictInitialized.current) {\n      console.log('Adding user name to dictionary:', userName)\n      addEntry(userName)\n      isDictInitialized.current = true\n    }\n  }, [userName, isDictInitialized, addEntry])\n\n  useEffect(() => {\n    initializeOnboarding()\n  }, [initializeOnboarding])\n\n  // Check server health on component mount and every 5 seconds\n  useEffect(() => {\n    const checkHealth = async () => {\n      const { isHealthy } = await checkLocalServerHealth()\n      setIsServerHealthy(isHealthy)\n    }\n\n    // Initial check\n    checkHealth()\n\n    // Set up periodic checks every 5 seconds\n    const intervalId = setInterval(checkHealth, 5000)\n\n    // Cleanup interval on unmount\n    return () => {\n      clearInterval(intervalId)\n    }\n  }, [])\n\n  const handleSelfHosted = async () => {\n    try {\n      await loginWithSelfHosted()\n    } catch (error) {\n      console.error('Self-hosted authentication failed:', error)\n    }\n  }\n\n  const onClickSelfHosted = async () => {\n    if (!isServerHealthy) {\n      setIsSelfHostedModalOpen(true)\n      return\n    }\n    await handleSelfHosted()\n  }\n\n  const handleSocialAuth = async (provider: string) => {\n    try {\n      switch (provider) {\n        case 'google':\n          await loginWithGoogle()\n          break\n        case 'microsoft':\n          await loginWithMicrosoft()\n          break\n        case 'apple':\n          await loginWithApple()\n          break\n        case 'github':\n          await loginWithGitHub()\n          break\n        default:\n          console.error('Unknown auth provider:', provider)\n      }\n    } catch (error) {\n      console.error(`${provider} authentication failed:`, error)\n    }\n  }\n\n  const handleContinueWithEmail = async () => {\n    if (!emailOk) {\n      setEmailTouched(true)\n      return\n    }\n    try {\n      setIsCheckingEmail(true)\n      setCheckError(null)\n      const res = await window.api.invoke('auth0-check-email', { email })\n      if (!res?.success) {\n        setCheckError(res?.error || 'Unable to check email')\n        return\n      }\n      if (res.exists) {\n        if (res.verified) {\n          setShowEmailLogin(true)\n        } else {\n          setCheckEmailDbUserId(res.dbUserId || null)\n          setShowCheckEmail(true)\n        }\n      } else {\n        setShowEmailPassword(true)\n      }\n    } finally {\n      setIsCheckingEmail(false)\n    }\n  }\n\n  if (showEmailPassword) {\n    return (\n      <EmailSignupContent\n        initialEmail={email}\n        onBack={() => setShowEmailPassword(false)}\n        onContinue={em => signupWithEmail(em)}\n      />\n    )\n  }\n\n  if (showEmailLogin) {\n    return (\n      <EmailLoginContent\n        initialEmail={email}\n        onBack={() => setShowEmailLogin(false)}\n        onContinue={() => {}}\n      />\n    )\n  }\n\n  if (showCheckEmail) {\n    return (\n      <CheckEmailContent\n        email={email}\n        dbUserId={checkEmailDbUserId}\n        onUseAnotherEmail={() => setShowCheckEmail(false)}\n        onRequireLogin={() => {\n          setShowCheckEmail(false)\n          setShowEmailLogin(true)\n        }}\n        password={null}\n      />\n    )\n  }\n\n  const emailOk = isValidEmail(email)\n\n  return (\n    <div className=\"flex flex-col h-full w-full bg-background items-center justify-center\">\n      <div className=\"relative flex flex-col items-center w-full h-full max-h-full px-8 py-16 mt-12 mb-12\">\n        {/* Logo */}\n        <div className=\"mb-4 bg-black rounded-md p-2 w-10 h-10\">\n          <ItoIcon height={24} width={24} style={{ color: '#FFFFFF' }} />\n        </div>\n\n        {/* Title and subtitle */}\n        <div className=\"text-center mb-10\">\n          <h1 className=\"text-3xl font-semibold mb-3 text-foreground\">\n            Get started with Ito\n          </h1>\n          <p className=\"text-muted-foreground text-base\">\n            Smart dictation. Everywhere you want.\n          </p>\n        </div>\n\n        {/* Social auth buttons */}\n        <div className=\"w-1/2 space-y-3 mb-6\">\n          <div className=\"grid grid-cols-2 gap-3\">\n            <Button\n              variant=\"outline\"\n              className=\"w-full h-12 flex items-center justify-start gap-3 text-sm font-medium\"\n              onClick={() => handleSocialAuth('google')}\n            >\n              <GoogleIcon className=\"size-5\" />\n              <div className=\"w-full text-sm font-medium\">\n                Continue with Google\n              </div>\n            </Button>\n\n            <Button\n              variant=\"outline\"\n              className=\"w-full h-12 flex items-center justify-start gap-3 text-sm font-medium\"\n              onClick={() => handleSocialAuth('microsoft')}\n            >\n              <MicrosoftIcon className=\"size-5\" />\n              <div className=\"w-full text-sm font-medium\">\n                Continue with Microsoft\n              </div>\n            </Button>\n          </div>\n\n          <div className=\"grid grid-cols-2 gap-3\">\n            <Button\n              variant=\"outline\"\n              className=\"h-12 flex items-center justify-start gap-2 text-sm font-medium\"\n              onClick={() => handleSocialAuth('apple')}\n            >\n              <AppleIcon className=\"size-5\" />\n              <div className=\"w-full text-sm font-medium\">\n                Continue with Apple\n              </div>\n            </Button>\n\n            <Button\n              variant=\"outline\"\n              className=\"h-12 flex items-center justify-start gap-2 text-sm font-medium\"\n              onClick={() => handleSocialAuth('github')}\n            >\n              <GitHubIcon className=\"size-5\" />\n              <div className=\"w-full text-sm font-medium\">\n                Continue with GitHub\n              </div>\n            </Button>\n          </div>\n        </div>\n\n        {/* Divider */}\n        <div className=\"w-1/2 flex items-center my-6\">\n          <div className=\"flex-1 border-t border-border\"></div>\n          <span className=\"px-4 text-xs text-muted-foreground\">OR</span>\n          <div className=\"flex-1 border-t border-border\"></div>\n        </div>\n\n        {/* Email sign up */}\n        <div className=\"w-1/2 space-y-3 mb-6\">\n          <input\n            type=\"email\"\n            placeholder=\"Email address\"\n            onChange={e => setEmail(e.target.value)}\n            onBlur={() => setEmailTouched(true)}\n            onKeyDown={e => {\n              if (e.key === 'Enter') {\n                e.preventDefault()\n                handleContinueWithEmail()\n              }\n            }}\n            aria-invalid={emailTouched && !emailOk}\n            aria-describedby={\n              emailTouched && !emailOk ? 'signup-email-error' : undefined\n            }\n            className={`w-full h-12 px-3 rounded-md border bg-background text-foreground placeholder:text-muted-foreground ${\n              emailTouched && !emailOk ? 'border-destructive' : 'border-border'\n            }`}\n          />\n          {emailTouched && !emailOk && (\n            <p id=\"signup-email-error\" className=\"text-xs text-destructive\">\n              Please enter a valid email address\n            </p>\n          )}\n          <Button\n            className=\"w-full h-12 text-sm font-medium\"\n            disabled={!emailOk || isCheckingEmail}\n            aria-busy={isCheckingEmail}\n            onClick={handleContinueWithEmail}\n          >\n            {isCheckingEmail ? 'Checking…' : 'Continue with email'}\n          </Button>\n          {checkError && (\n            <p className=\"text-xs text-destructive\">{checkError}</p>\n          )}\n        </div>\n\n        {/* Self-hosted option (icon + label in a row, pinned near bottom) */}\n        <div className=\"absolute bottom-0 left-1/2 -translate-x-1/2 flex items-center justify-center\">\n          <button\n            type=\"button\"\n            onClick={onClickSelfHosted}\n            className=\"flex flex-row items-center gap-2 hover:text-muted-foreground\"\n          >\n            <img src={UserCog} alt=\"User settings\" className=\"h-4 w-4\" />\n            <span className=\"text-sm\">Self-Hosted</span>\n          </button>\n        </div>\n\n        {/* Self-hosted modal */}\n        <Dialog\n          open={isSelfHostedModalOpen}\n          onOpenChange={setIsSelfHostedModalOpen}\n        >\n          <DialogContent\n            showCloseButton={false}\n            className=\"w-[90vw] max-w-[600px] rounded-md border-0 bg-white p-6\"\n          >\n            <DialogHeader className=\"mb-2 text-left\">\n              <DialogTitle className=\"text-[18px] leading-6 font-semibold text-black\">\n                Self-Hosted\n              </DialogTitle>\n              <DialogDescription className=\"text-sm leading-5 text-black\">\n                Local server must be running to use self-hosted option\n              </DialogDescription>\n            </DialogHeader>\n\n            <div className=\"rounded-md bg-[#F5F5F5] p-4\">\n              <p className=\"text-sm font-medium leading-5 text-black\">\n                Running Ito locally requires additional setup. Please refer to\n                our Github and Documentation\n              </p>\n              <div className=\"mt-4 flex w-full gap-4\">\n                <Button\n                  variant=\"outline\"\n                  asChild\n                  className=\"h-10 flex-1 basis-1/2 justify-center rounded border border-black text-sm font-medium text-black\"\n                >\n                  <a\n                    href={EXTERNAL_LINKS.GITHUB}\n                    target=\"_blank\"\n                    rel=\"noreferrer\"\n                  >\n                    Github\n                  </a>\n                </Button>\n                <Button\n                  variant=\"outline\"\n                  asChild\n                  className=\"h-10 flex-1 basis-1/2 justify-center rounded border border-black text-base font-medium text-black\"\n                >\n                  <a\n                    href={EXTERNAL_LINKS.WEBSITE}\n                    target=\"_blank\"\n                    rel=\"noreferrer\"\n                  >\n                    Documentation\n                  </a>\n                </Button>\n              </div>\n            </div>\n          </DialogContent>\n        </Dialog>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/welcome/contents/DataControlContent.tsx",
    "content": "import { Button } from '@/app/components/ui/button'\nimport { CheckCircle, Lock } from '@mynaui/icons-react'\nimport { EXTERNAL_LINKS } from '@/lib/constants/external-links'\nimport { useOnboardingStore } from '@/app/store/useOnboardingStore'\nimport { useSettingsStore } from '@/app/store/useSettingsStore'\n\nexport default function DataControlContent() {\n  const { incrementOnboardingStep, decrementOnboardingStep } =\n    useOnboardingStore()\n  const { shareAnalytics, setShareAnalytics } = useSettingsStore()\n\n  return (\n    <div className=\"flex flex-row h-full w-full bg-background\">\n      <div className=\"flex flex-col w-[45%] justify-center items-start pl-24\">\n        <div className=\"flex flex-col h-full min-h-[400px] justify-between py-12\">\n          <div className=\"mt-8\">\n            <button\n              className=\"mb-4 text-sm text-muted-foreground hover:underline\"\n              type=\"button\"\n              onClick={decrementOnboardingStep}\n            >\n              &lt; Back\n            </button>\n            <h1 className=\"text-3xl mb-4 mt-12\">You control your data.</h1>\n            <div className=\"flex flex-col gap-4 my-8 pr-24\">\n              <div\n                className={`border rounded-lg p-4 cursor-pointer transition-all ${shareAnalytics ? 'border-green-200 bg-green-50 border-2' : 'border-border border-2 bg-background'}`}\n                onClick={() => setShareAnalytics(true)}\n              >\n                <div className=\"flex items-center justify-between w-full mb-2\">\n                  <div className=\"font-medium\">Help improve Ito</div>\n                  {shareAnalytics && (\n                    <div>\n                      <CheckCircle\n                        style={{ color: '#22c55e', width: 18, height: 18 }}\n                      />\n                    </div>\n                  )}\n                </div>\n                <div className=\"text-sm text-muted-foreground max-w-md mt-1\">\n                  To make Ito better, this option lets us collect your audio,\n                  transcript, and edits to evaluate, train and improve Ito's\n                  features and AI models.\n                </div>\n              </div>\n              <div\n                className={`border rounded-lg p-4 cursor-pointer transition-all ${!shareAnalytics ? 'border-purple-200 bg-purple-50 border-2' : 'border-border border-2 bg-background'}`}\n                onClick={() => setShareAnalytics(false)}\n              >\n                <div className=\"flex items-center justify-between w-full mb-2\">\n                  <div className=\"font-medium\">Privacy Mode</div>\n                  {!shareAnalytics && (\n                    <div>\n                      <Lock\n                        style={{ color: '#a78bfa', width: 18, height: 18 }}\n                      />\n                    </div>\n                  )}\n                </div>\n                <div className=\"text-muted-foreground max-w-md mt-1\">\n                  If you enable Privacy Mode, none of your dictation data will\n                  be stored or used for model training by us or any third party.\n                </div>\n              </div>\n            </div>\n            <div className=\"text-sm text-muted-foreground\">\n              You can always change this later in settings.{' '}\n              <button\n                onClick={() =>\n                  window.api?.invoke(\n                    'web-open-url',\n                    EXTERNAL_LINKS.PRIVACY_POLICY,\n                  )\n                }\n                className=\"underline hover:text-foreground cursor-pointer\"\n              >\n                Read more here.\n              </button>\n            </div>\n          </div>\n          <div className=\"flex flex-col items-start mb-8\">\n            <Button className=\"w-24\" onClick={incrementOnboardingStep}>\n              Continue\n            </Button>\n          </div>\n        </div>\n      </div>\n      <div className=\"flex w-[55%] items-center justify-center bg-gradient-to-b from-purple-50/10 to-purple-100 border-l-2 border-purple-100\">\n        <Lock style={{ width: 220, height: 220, color: '#c4b5fd' }} />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/welcome/contents/EmailLoginContent.tsx",
    "content": "import { useMemo, useState } from 'react'\nimport { Button } from '@/app/components/ui/button'\nimport { AppOrbitImage } from '@/app/components/ui/app-orbit-image'\nimport { isValidEmail, isStrongPassword } from '@/app/utils/utils'\nimport { useAuth } from '@/app/components/auth/useAuth'\n\ntype Props = {\n  initialEmail?: string\n  onBack: () => void\n  onContinue: (email: string, password?: string) => void\n}\n\nexport default function EmailLoginContent({\n  initialEmail = '',\n  onBack,\n}: Props) {\n  const [email, setEmail] = useState(initialEmail)\n  const [password, setPassword] = useState('')\n\n  const emailOk = useMemo(() => isValidEmail(email), [email])\n\n  const isValid = useMemo(() => {\n    const passwordOk = isStrongPassword(password)\n    return emailOk && passwordOk\n  }, [emailOk, password])\n\n  const { loginWithEmailPassword } = useAuth()\n  const [isLoggingIn, setIsLoggingIn] = useState(false)\n  const [errorMessage, setErrorMessage] = useState<string | null>(null)\n\n  const handleLogin = async () => {\n    if (!emailOk || !isStrongPassword(password)) return\n    try {\n      setIsLoggingIn(true)\n      setErrorMessage(null)\n      await loginWithEmailPassword(email, password, { skipNavigate: true })\n    } catch (e: any) {\n      const msg = typeof e?.message === 'string' ? e.message : 'Login failed.'\n      console.error('Login error:', e)\n      setErrorMessage(msg)\n    } finally {\n      setIsLoggingIn(false)\n    }\n  }\n\n  return (\n    <div className=\"flex h-full w-full bg-background\">\n      {/* Left: form */}\n      <div className=\"flex w-1/2 flex-col justify-center px-16\">\n        {/* Back */}\n        <button\n          onClick={onBack}\n          className=\"mb-6 w-fit text-sm text-muted-foreground hover:underline\"\n        >\n          Back\n        </button>\n\n        {/* Heading */}\n        <div className=\"mb-8\">\n          <h1 className=\"text-3xl font-semibold text-foreground\">\n            Welcome back!\n          </h1>\n          <p className=\"mt-2 text-sm text-muted-foreground\">\n            Log in to get started\n          </p>\n        </div>\n\n        {/* Fields */}\n        <div className=\"space-y-5\">\n          <div className=\"flex flex-col gap-2\">\n            <label className=\"text-sm text-foreground\">Email</label>\n            <input\n              type=\"email\"\n              placeholder=\"Enter your email\"\n              value={email}\n              onChange={e => setEmail(e.target.value)}\n              className=\"h-10 w-full rounded-md border border-border bg-background px-3 text-foreground placeholder:text-muted-foreground\"\n            />\n          </div>\n\n          <div className=\"flex flex-col gap-2\">\n            <label className=\"text-sm text-foreground\">Password</label>\n            <input\n              type=\"password\"\n              placeholder=\"Enter your password\"\n              value={password}\n              onKeyDown={e => {\n                if (e.key === 'Enter') {\n                  e.preventDefault()\n                  handleLogin()\n                }\n              }}\n              onChange={e => setPassword(e.target.value)}\n              className=\"h-10 w-full rounded-md border border-border bg-background px-3 text-foreground placeholder:text-muted-foreground\"\n            />\n          </div>\n\n          <Button\n            className=\"h-10 w-full\"\n            disabled={!isValid || isLoggingIn}\n            aria-busy={isLoggingIn}\n            onClick={handleLogin}\n          >\n            {isLoggingIn && (\n              <span className=\"mr-2 inline-block size-4 rounded-full border-2 border-current border-t-transparent animate-spin\" />\n            )}\n            {isLoggingIn ? 'Logging in…' : 'Log In'}\n          </Button>\n\n          {errorMessage && (\n            <p className=\"mt-2 text-sm text-destructive\">{errorMessage}</p>\n          )}\n\n          <div className=\"flex items-center justify-between text-xs text-muted-foreground\">\n            <button className=\"hover:underline\" onClick={onBack}>\n              Log in with a different email\n            </button>\n            <span className=\"hover:underline cursor-default\">\n              Forgot password?\n            </span>\n          </div>\n        </div>\n      </div>\n\n      {/* Right: orbit illustration */}\n      <div className=\"flex w-1/2 items-center justify-center border-l border-border bg-muted/20\">\n        <AppOrbitImage />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/welcome/contents/EmailSignupContent.tsx",
    "content": "import { useMemo, useState } from 'react'\nimport { Button } from '@/app/components/ui/button'\nimport { AppOrbitImage } from '@/app/components/ui/app-orbit-image'\nimport { isValidEmail, isStrongPassword } from '@/app/utils/utils'\nimport { useAuth } from '@/app/components/auth/useAuth'\nimport CheckEmailContent from './CheckEmailContent'\nimport { EXTERNAL_LINKS } from '@/lib/constants/external-links'\n\ntype Props = {\n  initialEmail?: string\n  onBack: () => void\n  onContinue: (email: string, password?: string) => void\n}\n\nexport default function EmailSignupContent({\n  initialEmail = '',\n  onBack,\n}: Props) {\n  const email = initialEmail\n  const [fullName, setFullName] = useState('')\n  const [password, setPassword] = useState('')\n\n  const emailOk = useMemo(() => isValidEmail(email), [email])\n\n  const isValid = useMemo(() => {\n    const passwordOk = isStrongPassword(password)\n    const nameOk = fullName.trim().length > 0\n    return emailOk && passwordOk && nameOk\n  }, [emailOk, password, fullName])\n\n  const { createDatabaseUser } = useAuth()\n  const [showCheckEmail, setShowCheckEmail] = useState(false)\n  const [isCreating, setIsCreating] = useState(false)\n  const [errorMessage, setErrorMessage] = useState<string | null>(null)\n  const [dbUserId, setDbUserId] = useState<string | null>(null)\n\n  const handleCreate = async () => {\n    if (!emailOk || !isStrongPassword(password) || !fullName.trim()) return\n    try {\n      setIsCreating(true)\n      setErrorMessage(null)\n      const res = await createDatabaseUser(email, password, fullName.trim())\n      setDbUserId(`auth0|${res._id}`)\n      setShowCheckEmail(true)\n    } finally {\n      setIsCreating(false)\n    }\n  }\n\n  const handleCreateSafe = async () => {\n    try {\n      await handleCreate()\n    } catch (e: any) {\n      const msg = typeof e?.message === 'string' ? e.message : 'Signup failed.'\n      console.error('Signup error:', e)\n      setErrorMessage(msg)\n    }\n  }\n\n  if (showCheckEmail) {\n    return (\n      <CheckEmailContent\n        email={email}\n        password={password}\n        dbUserId={dbUserId}\n        onUseAnotherEmail={onBack}\n      />\n    )\n  }\n\n  return (\n    <div className=\"flex h-full w-full bg-background\">\n      {/* Left: form */}\n      <div className=\"flex w-1/2 flex-col justify-center px-16\">\n        {/* Back */}\n        <button\n          onClick={onBack}\n          className=\"mb-6 w-fit text-sm text-muted-foreground hover:underline\"\n        >\n          Back\n        </button>\n\n        {/* Heading */}\n        <div className=\"mb-8\">\n          <h1 className=\"text-3xl font-semibold text-foreground\">\n            Create your account\n          </h1>\n          <p className=\"mt-2 text-sm text-muted-foreground\">\n            This will take just a minute\n          </p>\n        </div>\n\n        {/* Fields */}\n        <div className=\"space-y-5\">\n          <div className=\"flex flex-col gap-2\">\n            <label className=\"text-sm text-foreground\">Email</label>\n            <div className=\"h-10 w-full rounded-md border border-border bg-muted px-3 text-foreground flex items-center\">\n              <span className=\"truncate\" title={email}>\n                {email}\n              </span>\n            </div>\n          </div>\n\n          <div className=\"flex flex-col gap-2\">\n            <label className=\"text-sm text-foreground\">Full name</label>\n            <input\n              type=\"text\"\n              placeholder=\"Enter your Full name\"\n              value={fullName}\n              onChange={e => setFullName(e.target.value)}\n              className=\"h-10 w-full rounded-md border border-border bg-background px-3 text-foreground placeholder:text-muted-foreground\"\n            />\n          </div>\n\n          <div className=\"flex flex-col gap-2\">\n            <label className=\"text-sm text-foreground\">Password</label>\n            <input\n              type=\"password\"\n              placeholder=\"Enter your password\"\n              value={password}\n              onKeyDown={e => {\n                if (e.key === 'Enter') {\n                  e.preventDefault()\n                  handleCreate()\n                }\n              }}\n              onChange={e => setPassword(e.target.value)}\n              className=\"h-10 w-full rounded-md border border-border bg-background px-3 text-foreground placeholder:text-muted-foreground\"\n            />\n            <p className=\"text-xs text-muted-foreground\">\n              Must be 8+ chars, include upper, lower, and number\n            </p>\n          </div>\n\n          <Button\n            className=\"h-10 w-full\"\n            disabled={!isValid || isCreating}\n            aria-busy={isCreating}\n            onClick={handleCreateSafe}\n          >\n            {isCreating && (\n              <span className=\"mr-2 inline-block size-4 rounded-full border-2 border-current border-t-transparent animate-spin\" />\n            )}\n            {isCreating ? 'Creating…' : 'Create Account'}\n          </Button>\n\n          {errorMessage && (\n            <p className=\"mt-2 text-sm text-destructive\">{errorMessage}</p>\n          )}\n\n          <p className=\"text-center text-xs text-muted-foreground\">\n            By continuing, you agree to our{' '}\n            <a\n              href={EXTERNAL_LINKS.WEBSITE}\n              target=\"_blank\"\n              rel=\"noreferrer\"\n              className=\"underline\"\n            >\n              Terms\n            </a>{' '}\n            and{' '}\n            <a\n              href={EXTERNAL_LINKS.PRIVACY_POLICY}\n              target=\"_blank\"\n              rel=\"noreferrer\"\n              className=\"underline\"\n            >\n              Privacy Policy\n            </a>\n          </p>\n        </div>\n      </div>\n\n      {/* Right: orbit illustration */}\n      <div className=\"flex w-1/2 items-center justify-center border-l border-border bg-muted/20\">\n        <AppOrbitImage />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/welcome/contents/GoodToGoContent.tsx",
    "content": "import { Button } from '@/app/components/ui/button'\nimport { useOnboardingStore } from '@/app/store/useOnboardingStore'\nimport { CheckCircle } from '@mynaui/icons-react'\n\nexport default function GoodToGoContent() {\n  const { incrementOnboardingStep, decrementOnboardingStep } =\n    useOnboardingStore()\n\n  return (\n    <div className=\"flex flex-row h-full w-full bg-background\">\n      <div className=\"flex flex-col w-[45%] justify-center items-start px-24\">\n        <div className=\"flex flex-col h-full min-h-[400px] justify-between py-12\">\n          <div className=\"mt-8\">\n            <button\n              className=\"mb-4 text-sm text-muted-foreground hover:underline\"\n              type=\"button\"\n              onClick={decrementOnboardingStep}\n            >\n              &lt; Back\n            </button>\n            <h1 className=\"text-3xl mb-4 mt-12\">\n              Your hardware setup is good to go!\n            </h1>\n          </div>\n          <div className=\"flex flex-col items-start mb-8\">\n            <Button className=\"w-24\" onClick={incrementOnboardingStep}>\n              Continue\n            </Button>\n          </div>\n        </div>\n      </div>\n      <div className=\"flex w-[55%] items-center justify-center bg-gradient-to-b from-purple-50/10 to-purple-100 border-l-2 border-purple-100\">\n        <CheckCircle style={{ width: 220, height: 220, color: '#a78bfa' }} />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/welcome/contents/IntroducingIntelligentModeContent.tsx",
    "content": "import { Button } from '@/app/components/ui/button'\nimport { useOnboardingStore } from '@/app/store/useOnboardingStore'\nimport { CheckCircle } from '@mynaui/icons-react'\nimport { ArrowRight } from 'lucide-react'\nimport KeyboardShortcutEditor from '../../ui/keyboard-shortcut-editor'\nimport { ItoMode } from '@/app/generated/ito_pb'\nimport { Tip } from '../../ui/tip'\nimport { useSettingsStore } from '@/app/store/useSettingsStore'\n\nexport default function IntroducingIntelligentMode() {\n  const { incrementOnboardingStep, decrementOnboardingStep } =\n    useOnboardingStore()\n\n  const { getItoModeShortcuts, updateKeyboardShortcut } = useSettingsStore()\n  const keyboardShortcut = getItoModeShortcuts(ItoMode.EDIT)[0]\n\n  return (\n    <div className=\"flex flex-row h-full w-full bg-background\">\n      <div className=\"flex flex-col w-[45%] justify-center items-start px-24\">\n        <div className=\"flex flex-col h-full min-h-[400px] justify-between py-12\">\n          <div className=\"mt-8\">\n            <button\n              className=\"mb-4 text-sm text-muted-foreground hover:underline\"\n              type=\"button\"\n              onClick={decrementOnboardingStep}\n            >\n              &lt; Back\n            </button>\n            <div className=\"text-2xl mb-1 font-medium\">\n              Introducing Ito Intelligent Mode\n            </div>\n            <div className=\"mb-4 text-lg font-light\">\n              What you ask gets written.\n            </div>\n            {[\n              'Press Hotkey -> Speak to Ito',\n              'Ito send your speech to LLM',\n              'Pastes LLM output into text box',\n            ].map((step, index) => (\n              <div\n                key={index}\n                className=\"flex items-center gap-2 text-base font-light mb-1\"\n              >\n                <CheckCircle />\n                {step}\n              </div>\n            ))}\n            <div className=\"text-lg font-semibold mt-6 mb-1\">Examples</div>\n            {[\n              \"Write an email to Jeff confirming tomorrow's meeting\",\n              'Write a detailed prompt to create a picture of a tall New York building',\n              'Write a detailed prompt to create a stunning landing page for a dictation app',\n            ].map((step, index) => (\n              <div\n                key={index}\n                className=\"flex items-center gap-2 text-base font-light mb-1 italic\"\n              >\n                <ArrowRight className=\"h-5 w-5 shrink-0 text-black\" />\n                {step}\n              </div>\n            ))}\n            <Tip\n              tipText=\"You can also trigger Intelligent Mode by saying 'Hey Ito' when using the regular dictation hotkey.\"\n              className=\"mt-3\"\n            />\n          </div>\n\n          <div className=\"flex flex-col items-start mb-8\">\n            <Button className=\"w-24\" onClick={incrementOnboardingStep}>\n              Continue\n            </Button>\n          </div>\n        </div>\n      </div>\n      <div className=\"flex w-[55%] items-center justify-center bg-gradient-to-b from-purple-50/10 to-purple-100 border-l-2 border-purple-100\">\n        <KeyboardShortcutEditor\n          shortcut={keyboardShortcut}\n          onShortcutChange={updateKeyboardShortcut}\n          keySize={80}\n          editButtonText=\"Change Shortcut\"\n          showConfirmButton={true}\n          onConfirm={incrementOnboardingStep}\n          editModeTitle=\"Press a key to add it to the shortcut, press it again to remove it\"\n          viewModeTitle=\"Default Hotkey to activate Intelligent Mode\"\n          minHeight={112}\n          editButtonClassName=\"w-44\"\n          confirmButtonClassName=\"hidden\"\n          className=\"rounded-xl shadow-lg p-6 flex flex-col items-center min-w-[500px] max-h-[280px]\"\n          mode={ItoMode.EDIT}\n        />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/welcome/contents/KeyboardTestContext.tsx",
    "content": "import { useOnboardingStore } from '@/app/store/useOnboardingStore'\nimport { useSettingsStore } from '@/app/store/useSettingsStore'\nimport KeyboardShortcutEditor from '../../ui/keyboard-shortcut-editor'\nimport { ItoMode } from '@/app/generated/ito_pb'\nimport { getItoModeShortcutDefaults } from '@/lib/constants/keyboard-defaults'\nimport { usePlatform } from '@/app/hooks/usePlatform'\nimport { getKeyDisplay } from '@/app/utils/keyboard'\nimport { KeyName } from '@/lib/types/keyboard'\nimport React from 'react'\n\nexport default function KeyboardTestContent() {\n  const { incrementOnboardingStep, decrementOnboardingStep } =\n    useOnboardingStore()\n  const { getItoModeShortcuts, updateKeyboardShortcut } = useSettingsStore()\n  const keyboardShortcut = getItoModeShortcuts(ItoMode.TRANSCRIBE)[0]\n  const platform = usePlatform()\n  const defaultKeys = getItoModeShortcutDefaults(platform)[ItoMode.TRANSCRIBE]\n\n  return (\n    <div className=\"flex flex-row h-full w-full bg-background\">\n      <div className=\"flex flex-col w-[45%] justify-center items-start px-24\">\n        <div className=\"flex flex-col h-full min-h-[400px] justify-between py-12 overflow-hidden\">\n          <div className=\"mt-8\">\n            <button\n              className=\"mb-4 text-sm text-muted-foreground hover:underline\"\n              type=\"button\"\n              onClick={decrementOnboardingStep}\n            >\n              &lt; Back\n            </button>\n            <h1 className=\"text-3xl mb-4 mt-12\">\n              Press the keyboard shortcut to test it out.\n            </h1>\n            <div className=\"text-base text-muted-foreground mb-8 max-w-md\">\n              <span key=\"we-recommend\">We recommend the </span>\n              {defaultKeys.map((key, index) => (\n                <React.Fragment key={index}>\n                  <span className=\"inline-flex items-center px-2 py-0.5 bg-neutral-100 border rounded text-xs font-mono ml-1\">\n                    {getKeyDisplay(key as KeyName, platform, {\n                      showDirectionalText: false,\n                      format: 'label',\n                    })}\n                  </span>\n                  <span>{index < defaultKeys.length - 1 && ' + '}</span>\n                </React.Fragment>\n              ))}\n              <span key=\"at-bottom\">\n                {' '}\n                key at the bottom left of the keyboard\n              </span>\n            </div>\n          </div>\n        </div>\n      </div>\n      <div className=\"flex w-[55%] items-center justify-center bg-gradient-to-b from-purple-50/10 to-purple-100 border-l-2 border-purple-100\">\n        <KeyboardShortcutEditor\n          shortcut={keyboardShortcut}\n          onShortcutChange={updateKeyboardShortcut}\n          keySize={80}\n          editButtonText=\"No, change shortcut\"\n          confirmButtonText=\"Yes\"\n          showConfirmButton={true}\n          onConfirm={incrementOnboardingStep}\n          editModeTitle=\"Press a key to add it to the shortcut, press it again to remove it\"\n          viewModeTitle=\"Does the button turn purple while pressing it?\"\n          minHeight={112}\n          editButtonClassName=\"w-44\"\n          confirmButtonClassName=\"w-16\"\n          className=\"rounded-xl shadow-lg p-6 flex flex-col items-center min-w-[500px] max-h-[280px]\"\n          mode={ItoMode.TRANSCRIBE}\n        />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/welcome/contents/MicrophoneTestContent.tsx",
    "content": "import { Button } from '@/app/components/ui/button'\nimport { useEffect, useState } from 'react'\nimport { useOnboardingStore } from '@/app/store/useOnboardingStore'\nimport { useSettingsStore } from '@/app/store/useSettingsStore'\nimport { MicrophoneSelector } from '@/app/components/ui/microphone-selector'\n\nfunction MicrophoneBars({ volume }: { volume: number }) {\n  const minHeight = 0.2\n  const levels = Array(12)\n    .fill(0)\n    .map((_, i) => {\n      const threshold = (i / 12) * 0.5\n      const normalizedVolume = Math.min(volume * 8, 1)\n      return normalizedVolume > threshold ? 1 : minHeight\n    })\n\n  return (\n    <div\n      className=\"flex gap-1 py-4 px-4 items-end bg-neutral-100 rounded-md\"\n      style={{ height: 120 }}\n    >\n      {levels.map((level, i) => (\n        <div\n          key={i}\n          className={`mx-2 h-full ${level > minHeight ? 'bg-purple-300' : 'bg-neutral-300'}`}\n          style={{\n            width: 18,\n            borderRadius: 6,\n            transition: 'height 0.18s cubic-bezier(.4,2,.6,1)',\n          }}\n        />\n      ))}\n    </div>\n  )\n}\n\nexport default function MicrophoneTestContent() {\n  const { incrementOnboardingStep, decrementOnboardingStep } =\n    useOnboardingStore()\n  const { microphoneDeviceId, microphoneName, setMicrophoneDeviceId } =\n    useSettingsStore()\n\n  const [volume, setVolume] = useState(0)\n  const [smoothedVolume, setSmoothedVolume] = useState(0)\n\n  // This effect listens for volume updates from the main process\n  useEffect(() => {\n    const unsubscribe = window.api.on('volume-update', (newVolume: number) => {\n      setVolume(newVolume)\n    })\n\n    // Cleanup the listener when the component unmounts\n    return () => {\n      unsubscribe()\n    }\n  }, []) // Runs only once on mount\n\n  // This effect manages the \"test\" recording lifecycle.\n  // It starts recording when a device is selected and stops when the component unmounts.\n  useEffect(() => {\n    if (microphoneDeviceId) {\n      console.log(`Starting test recording on device: ${microphoneDeviceId}`)\n      window.api.send('start-native-recording-test')\n    }\n\n    // Cleanup function: stop recording when the component unmounts or device changes\n    return () => {\n      console.log('Stopping test recording.')\n      // Use the test-specific stop handler that only stops audio recording\n      window.api.send('stop-native-recording-test')\n    }\n  }, [microphoneDeviceId]) // Re-runs whenever the selected microphone changes\n\n  // Smooth the volume updates to reduce flicker\n  useEffect(() => {\n    const smoothing = 0.4 // Lower = smoother, higher = more responsive\n    setSmoothedVolume(prev => prev * (1 - smoothing) + volume * smoothing)\n  }, [volume])\n\n  // Handles changing the microphone\n  const handleMicrophoneChange = async (deviceId: string, name: string) => {\n    // The useEffect hook above will automatically handle stopping the old\n    // stream and starting the new one when the deviceId changes.\n    setMicrophoneDeviceId(deviceId, name)\n  }\n\n  return (\n    <div className=\"flex flex-row h-full w-full bg-background\">\n      <div className=\"flex flex-col w-[45%] justify-center items-start px-24\">\n        <div className=\"flex flex-col h-full min-h-[400px] justify-between py-12 overflow-hidden\">\n          <div className=\"mt-8\">\n            <button\n              className=\"mb-4 text-sm text-muted-foreground hover:underline\"\n              type=\"button\"\n              onClick={decrementOnboardingStep}\n            >\n              &lt; Back\n            </button>\n            <h1 className=\"text-3xl mb-4 mt-12\">\n              Speak to test your microphone.\n            </h1>\n            <div className=\"text-base text-muted-foreground mb-8 max-w-md\">\n              Your computer's built-in mic will ensure accurate transcription\n              with minimal latency.\n            </div>\n          </div>\n        </div>\n      </div>\n      <div className=\"flex w-[55%] items-center justify-center bg-gradient-to-b from-purple-50/10 to-purple-100 border-l-2 border-purple-100\">\n        <div\n          className=\"bg-white rounded-xl shadow-lg p-6 flex flex-col items-center\"\n          style={{ minWidth: 500, maxHeight: 280 }}\n        >\n          <div className=\"text-lg font-medium mb-6 text-center\">\n            Do you see purple bars moving while you speak?\n          </div>\n          <MicrophoneBars volume={smoothedVolume} />\n          <div className=\"flex gap-2 mt-6 w-full justify-end\">\n            <MicrophoneSelector\n              selectedDeviceId={microphoneDeviceId}\n              selectedMicrophoneName={microphoneName}\n              onSelectionChange={handleMicrophoneChange}\n              triggerButtonText=\"No, change microphone\"\n              triggerButtonVariant=\"outline\"\n              triggerButtonClassName=\"w-44\"\n            />\n            <Button\n              className=\"w-16\"\n              type=\"button\"\n              onClick={incrementOnboardingStep}\n            >\n              Yes\n            </Button>\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/welcome/contents/PermissionsContent.tsx",
    "content": "import { Button } from '@/app/components/ui/button'\nimport { InfoCircle } from '@mynaui/icons-react'\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from '@/app/components/ui/tooltip'\nimport { useState, useEffect, useRef } from 'react'\nimport { Spinner } from '@/app/components/ui/spinner'\nimport { AnimatedCheck } from '@/app/components/ui/animated-checkmark'\nimport { Lock } from '@mynaui/icons-react'\nimport { usePermissionsStore } from '@/app/store/usePermissionsStore'\nimport { useOnboardingStore } from '@/app/store/useOnboardingStore'\nimport accessibilityVideo from '@/app/assets/accesssibility.webm'\nimport microphoneVideo from '@/app/assets/microphone.webm'\n\nexport default function PermissionsContent() {\n  const { incrementOnboardingStep, decrementOnboardingStep } =\n    useOnboardingStore()\n\n  const {\n    isAccessibilityEnabled,\n    isMicrophoneEnabled,\n    setAccessibilityEnabled,\n    setMicrophoneEnabled,\n  } = usePermissionsStore()\n  const [checkingAccessibility, setCheckingAccessibility] = useState(false)\n  const [checkingMicrophone, setCheckingMicrophone] = useState(false)\n  const pollingRef = useRef<NodeJS.Timeout | null>(null)\n  const microphonePollingRef = useRef<NodeJS.Timeout | null>(null)\n  const [accessibilityCheckTrigger, setAccessibilityCheckTrigger] =\n    useState(false)\n  const [microphoneCheckTrigger, setMicrophoneCheckTrigger] = useState(false)\n\n  useEffect(() => {\n    return () => {\n      if (pollingRef.current) {\n        clearInterval(pollingRef.current)\n      }\n      if (microphonePollingRef.current) {\n        clearInterval(microphonePollingRef.current)\n      }\n    }\n  }, [])\n\n  useEffect(() => {\n    window.api\n      .invoke('check-accessibility-permission', false)\n      .then((enabled: boolean) => {\n        setAccessibilityEnabled(enabled)\n      })\n\n    window.api\n      .invoke('check-microphone-permission', false)\n      .then((enabled: boolean) => {\n        setMicrophoneEnabled(enabled)\n      })\n  }, [setAccessibilityEnabled, setMicrophoneEnabled])\n\n  useEffect(() => {\n    if (isAccessibilityEnabled) {\n      console.log(\n        'Accessibility permission granted. Starting key listener service...',\n      )\n      window.api.invoke('start-key-listener-service')\n      setAccessibilityCheckTrigger(false)\n      setTimeout(() => setAccessibilityCheckTrigger(true), 100)\n    }\n  }, [isAccessibilityEnabled])\n\n  useEffect(() => {\n    if (isMicrophoneEnabled) {\n      setMicrophoneCheckTrigger(false)\n      setTimeout(() => setMicrophoneCheckTrigger(true), 100)\n    }\n  }, [isMicrophoneEnabled])\n\n  const pollAccessibility = () => {\n    pollingRef.current = setInterval(() => {\n      window.api\n        .invoke('check-accessibility-permission', false)\n        .then((enabled: boolean) => {\n          if (enabled) {\n            setAccessibilityEnabled(true)\n            setCheckingAccessibility(false)\n            if (pollingRef.current) {\n              clearInterval(pollingRef.current)\n              pollingRef.current = null\n            }\n          }\n        })\n    }, 2000)\n  }\n\n  const pollMicrophone = () => {\n    microphonePollingRef.current = setInterval(() => {\n      window.api\n        .invoke('check-microphone-permission', false)\n        .then((enabled: boolean) => {\n          if (enabled) {\n            setMicrophoneEnabled(true)\n            setCheckingMicrophone(false)\n            if (microphonePollingRef.current) {\n              clearInterval(microphonePollingRef.current)\n              microphonePollingRef.current = null\n            }\n          }\n        })\n    }, 2000)\n  }\n\n  const handleAllowAccessibility = () => {\n    setCheckingAccessibility(true)\n    window.api\n      .invoke('check-accessibility-permission', true)\n      .then((enabled: boolean) => {\n        setAccessibilityEnabled(enabled)\n        if (!enabled) {\n          pollAccessibility()\n        } else {\n          setCheckingAccessibility(false)\n        }\n      })\n  }\n\n  const handleAllowMicrophone = () => {\n    setCheckingMicrophone(true)\n    window.api\n      .invoke('check-microphone-permission', true)\n      .then((enabled: boolean) => {\n        setMicrophoneEnabled(enabled)\n        if (!enabled) {\n          pollMicrophone()\n        } else {\n          setCheckingMicrophone(false)\n        }\n      })\n  }\n\n  return (\n    <div className=\"flex flex-row h-full w-full bg-background\">\n      <div className=\"flex flex-col w-[45%] justify-center items-start pl-24\">\n        <div className=\"flex flex-col h-full min-h-[400px] justify-between py-12\">\n          <div className=\"mt-8\">\n            <button\n              className=\"mb-4 text-sm text-muted-foreground hover:underline\"\n              type=\"button\"\n              onClick={decrementOnboardingStep}\n            >\n              &lt; Back\n            </button>\n            <h1 className=\"text-3xl mb-4 mt-12 pr-24\">\n              {isAccessibilityEnabled && isMicrophoneEnabled\n                ? 'Thank you for trusting us. We take your privacy seriously.'\n                : 'Set up Ito on your computer'}\n            </h1>\n            <div className=\"flex flex-col gap-4 my-8 pr-24\">\n              <div className=\"border rounded-lg p-4 flex flex-col gap-2 bg-background border-border border-2\">\n                <div\n                  className={`flex items-center gap-2 ${isAccessibilityEnabled ? '' : 'mb-2'}`}\n                >\n                  {isAccessibilityEnabled && (\n                    <AnimatedCheck trigger={accessibilityCheckTrigger} />\n                  )}\n                  <div className=\"font-medium text-base flex\">\n                    {isAccessibilityEnabled\n                      ? 'Ito can insert and edit text.'\n                      : 'Allow Ito to insert spoken words.'}\n                  </div>\n                </div>\n                {!isAccessibilityEnabled && (\n                  <>\n                    <div className=\"text-sm text-muted-foreground mb-2\">\n                      This lets Ito put your spoken words in the right textbox\n                      and edit text according to your commands\n                    </div>\n                    <div className=\"flex items-center justify-between\">\n                      <div className=\"flex items-center gap-2 mt-1\">\n                        <Button\n                          className=\"w-24\"\n                          type=\"button\"\n                          onClick={handleAllowAccessibility}\n                        >\n                          Allow\n                        </Button>\n                        <Tooltip>\n                          <TooltipTrigger asChild>\n                            <span className=\"inline-flex items-center\">\n                              <InfoCircle style={{ width: 20, height: 20 }} />\n                            </span>\n                          </TooltipTrigger>\n                          <TooltipContent side=\"right\" align=\"start\">\n                            <p>\n                              Ito uses this to gather context based on the\n                              application you&apos;re using, <br /> and to\n                              access your clipboard temporarily to paste text.\n                            </p>\n                          </TooltipContent>\n                        </Tooltip>\n                      </div>\n                      {checkingAccessibility && (\n                        <div className=\"text-sm text-muted-foreground\">\n                          <Spinner size=\"medium\" />\n                        </div>\n                      )}\n                    </div>\n                  </>\n                )}\n              </div>\n              <div className=\"border rounded-lg p-4 flex flex-col gap-2 bg-background border-border border-2\">\n                <div\n                  className={`flex items-center gap-2 ${isMicrophoneEnabled ? '' : 'mb-2'}`}\n                >\n                  {isMicrophoneEnabled && (\n                    <AnimatedCheck trigger={microphoneCheckTrigger} />\n                  )}\n                  <div className=\"font-medium text-base flex\">\n                    {isMicrophoneEnabled\n                      ? 'Ito can use your microphone.'\n                      : 'Allow Ito to use your microphone.'}\n                  </div>\n                </div>\n                {isAccessibilityEnabled && !isMicrophoneEnabled && (\n                  <>\n                    <div className=\"text-sm text-muted-foreground mb-2\">\n                      This lets Ito hear your voice and transcribe your speech\n                    </div>\n                    <div className=\"flex items-center justify-between\">\n                      <div className=\"flex items-center gap-2 mt-1\">\n                        <Button\n                          className=\"w-24\"\n                          type=\"button\"\n                          onClick={handleAllowMicrophone}\n                        >\n                          Allow\n                        </Button>\n                        <Tooltip>\n                          <TooltipTrigger asChild>\n                            <span className=\"inline-flex items-center\">\n                              <InfoCircle style={{ width: 20, height: 20 }} />\n                            </span>\n                          </TooltipTrigger>\n                          <TooltipContent side=\"right\" align=\"start\">\n                            <p>\n                              Ito will show an animation when the mic is active{' '}\n                              <br /> and only listen when you activate it\n                            </p>\n                          </TooltipContent>\n                        </Tooltip>\n                      </div>\n                      {checkingMicrophone && (\n                        <div className=\"text-sm text-muted-foreground\">\n                          <Spinner size=\"medium\" />\n                        </div>\n                      )}\n                    </div>\n                  </>\n                )}\n              </div>\n            </div>\n          </div>\n          <div className=\"flex flex-col items-start mb-8\">\n            <Button\n              className={`w-24 ${isAccessibilityEnabled && isMicrophoneEnabled ? '' : 'hidden'}`}\n              onClick={incrementOnboardingStep}\n            >\n              Continue\n            </Button>\n          </div>\n        </div>\n      </div>\n      <div className=\"flex w-[55%] items-center justify-center bg-gradient-to-b from-purple-50/10 to-purple-100 border-l-2 border-purple-100\">\n        <div className=\"w-[600px] h-[500px] rounded-lg flex items-center justify-center\">\n          {isAccessibilityEnabled && isMicrophoneEnabled ? (\n            <Lock style={{ width: 220, height: 220, color: '#c4b5fd' }} />\n          ) : !isAccessibilityEnabled ? (\n            <video\n              src={accessibilityVideo}\n              autoPlay\n              loop\n              muted\n              className=\"max-w-full max-h-full object-contain rounded-lg\"\n            />\n          ) : (\n            <video\n              src={microphoneVideo}\n              autoPlay\n              loop\n              muted\n              className=\"max-w-full max-h-full object-contain\"\n            />\n          )}\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/welcome/contents/ReferralContent.tsx",
    "content": "import { Button } from '@/app/components/ui/button'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '../../ui/dropdown-menu'\nimport AvatarIcon from '../../icons/AvatarIcon'\nimport { useOnboardingStore } from '@/app/store/useOnboardingStore'\nimport { useAuthStore } from '@/app/store/useAuthStore'\n\nconst sources = [\n  'Twitter',\n  'TikTok',\n  'Instagram',\n  'Discord',\n  'YouTube',\n  'Reddit',\n  'Friend',\n  'Google Search',\n  'Product Hunt',\n  'Other',\n]\n\nexport default function ReferralContent() {\n  const { incrementOnboardingStep, referralSource, setReferralSource } =\n    useOnboardingStore()\n  const { user } = useAuthStore()\n  const firstName = user?.name?.split(' ')[0]\n\n  return (\n    <div className=\"flex flex-row h-full w-full bg-background\">\n      <div className=\"flex flex-col w-[45%] justify-center items-start pl-24\">\n        <div className=\"flex flex-col h-full min-h-[400px] justify-between py-12\">\n          <div className=\"pt-32\">\n            <h1 className=\"text-3xl mb-4\">\n              Welcome{firstName ? `, ${firstName}!` : '!'}\n            </h1>\n            <p className=\"mb-6 text-base text-muted-foreground\">\n              Where did you hear about us?\n            </p>\n            <DropdownMenu>\n              <DropdownMenuTrigger asChild>\n                <button className=\"mb-8 w-48 px-4 py-2 border border-border rounded-md bg-background text-base focus:outline-none text-left flex items-center justify-between\">\n                  {referralSource ? (\n                    <span className=\"text-sm\">{referralSource}</span>\n                  ) : (\n                    <span className=\"text-muted-foreground text-sm\">\n                      Select a source\n                    </span>\n                  )}\n                  <svg\n                    className=\"ml-2 h-4 w-4 text-muted-foreground\"\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    strokeWidth=\"2\"\n                    viewBox=\"0 0 24 24\"\n                  >\n                    <path\n                      strokeLinecap=\"round\"\n                      strokeLinejoin=\"round\"\n                      d=\"M19 9l-7 7-7-7\"\n                    />\n                  </svg>\n                </button>\n              </DropdownMenuTrigger>\n              <DropdownMenuContent className=\"w-48 text-sm border-border\">\n                {sources.map(s => (\n                  <DropdownMenuItem\n                    key={s}\n                    onSelect={() => setReferralSource(s)}\n                  >\n                    {s}\n                  </DropdownMenuItem>\n                ))}\n              </DropdownMenuContent>\n            </DropdownMenu>\n          </div>\n          <div className=\"flex flex-col items-start mb-8\">\n            <Button\n              className=\"w-24\"\n              onClick={incrementOnboardingStep}\n              disabled={!referralSource}\n            >\n              Continue\n            </Button>\n          </div>\n        </div>\n      </div>\n      <div className=\"flex w-[55%] items-center justify-center bg-gradient-to-b from-purple-50/10 to-purple-100 border-l-2 border-purple-100\">\n        <AvatarIcon />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/welcome/contents/SignInContent.tsx",
    "content": "import { Button } from '@/app/components/ui/button'\nimport {\n  Tooltip,\n  TooltipTrigger,\n  TooltipContent,\n} from '@/app/components/ui/tooltip'\nimport { useOnboardingStore } from '@/app/store/useOnboardingStore'\nimport ItoIcon from '../../icons/ItoIcon'\nimport GoogleIcon from '../../icons/GoogleIcon'\nimport AppleIcon from '../../icons/AppleIcon'\nimport GitHubIcon from '../../icons/GitHubIcon'\nimport MicrosoftIcon from '../../icons/MicrosoftIcon'\nimport { useEffect, useMemo, useState } from 'react'\nimport { useAuth } from '../../auth/useAuth'\nimport { checkLocalServerHealth } from '@/app/utils/healthCheck'\nimport { useAuthStore } from '@/app/store/useAuthStore'\nimport { useNotesStore } from '@/app/store/useNotesStore'\nimport { useDictionaryStore } from '@/app/store/useDictionaryStore'\nimport { AppOrbitImage } from '@/app/components/ui/app-orbit-image'\nimport { STORE_KEYS } from '../../../../lib/constants/store-keys'\nimport { isValidEmail, isStrongPassword } from '@/app/utils/utils'\n\n// Auth provider configuration\nconst AUTH_PROVIDERS = {\n  email: {\n    key: 'email',\n    label: 'Email',\n    icon: null,\n    variant: 'default' as const,\n  },\n  'google-oauth2': {\n    key: 'google',\n    label: 'Google',\n    icon: GoogleIcon,\n    variant: 'outline' as const,\n  },\n  microsoft: {\n    key: 'microsoft',\n    label: 'Microsoft',\n    icon: MicrosoftIcon,\n    variant: 'outline' as const,\n  },\n  apple: {\n    key: 'apple',\n    label: 'Apple',\n    icon: AppleIcon,\n    variant: 'outline' as const,\n  },\n  github: {\n    key: 'github',\n    label: 'GitHub',\n    icon: GitHubIcon,\n    variant: 'outline' as const,\n  },\n  'self-hosted': {\n    key: 'self-hosted',\n    label: 'Self-Hosted',\n    icon: null,\n    variant: 'default' as const,\n  },\n}\n\n// Reusable AuthButton component\ninterface AuthButtonProps {\n  provider: keyof typeof AUTH_PROVIDERS\n  onClick: () => void\n  className?: string\n  children?: React.ReactNode\n  disabled?: boolean\n  title?: string\n}\n\nfunction AuthButton({\n  provider,\n  onClick,\n  className = '',\n  children,\n  disabled = false,\n  title,\n}: AuthButtonProps) {\n  const config = AUTH_PROVIDERS[provider]\n  const IconComponent = config.icon\n\n  const button = (\n    <Button\n      variant={config.variant}\n      className={`h-12 flex items-center justify-center gap-3 text-sm font-medium ${className}`}\n      onClick={onClick}\n      disabled={disabled}\n    >\n      {IconComponent && <IconComponent className=\"size-5\" />}\n      <span>{children || config.label}</span>\n    </Button>\n  )\n\n  if (disabled && title) {\n    return (\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <div className=\"w-full\">{button}</div>\n        </TooltipTrigger>\n        <TooltipContent>\n          <p>{title}</p>\n        </TooltipContent>\n      </Tooltip>\n    )\n  }\n\n  return button\n}\n\nexport default function SignInContent() {\n  const { incrementOnboardingStep } = useOnboardingStore()\n  const { clearAuth } = useAuthStore()\n  const { loadNotes } = useNotesStore()\n  const { loadEntries } = useDictionaryStore()\n  const { resetOnboarding } = useOnboardingStore()\n  const [isServerHealthy, setIsServerHealthy] = useState(true)\n  const [email, setEmail] = useState('')\n  const [password, setPassword] = useState('')\n  const [isLoggingIn, setIsLoggingIn] = useState(false)\n  const [errorMessage, setErrorMessage] = useState<string | null>(null)\n\n  const {\n    user,\n    isAuthenticated,\n    loginWithGoogle,\n    loginWithMicrosoft,\n    loginWithApple,\n    loginWithGitHub,\n    loginWithSelfHosted,\n    loginWithEmail,\n    loginWithEmailPassword,\n  } = useAuth()\n\n  // Check server health on component mount and every 5 seconds\n  useEffect(() => {\n    const checkHealth = async () => {\n      const { isHealthy } = await checkLocalServerHealth()\n      setIsServerHealthy(isHealthy)\n    }\n\n    // Initial check\n    checkHealth()\n\n    // Set up periodic checks every 5 seconds\n    const intervalId = setInterval(checkHealth, 5000)\n\n    // Cleanup interval on unmount\n    return () => {\n      clearInterval(intervalId)\n    }\n  }, [])\n\n  // If user is authenticated, proceed to next step\n  useEffect(() => {\n    if (isAuthenticated && user) {\n      incrementOnboardingStep()\n    }\n  }, [isAuthenticated, user, incrementOnboardingStep])\n\n  const handleSelfHosted = async () => {\n    try {\n      await loginWithSelfHosted()\n    } catch (error) {\n      console.error('Self-hosted authentication failed:', error)\n    }\n  }\n\n  const handleSocialAuth = async (provider: string) => {\n    try {\n      switch (provider) {\n        case 'google':\n          await loginWithGoogle(userEmail)\n          break\n        case 'microsoft':\n          await loginWithMicrosoft(userEmail)\n          break\n        case 'apple':\n          await loginWithApple(userEmail)\n          break\n        case 'github':\n          await loginWithGitHub(userEmail)\n          break\n        default:\n          console.error('Unknown auth provider:', provider)\n      }\n    } catch (error) {\n      console.error(`${provider} authentication failed:`, error)\n    }\n  }\n\n  // Get stored user info for display\n  const storedUser = window.electron?.store?.get(STORE_KEYS.AUTH)?.user\n  const userEmail = storedUser?.email\n  const userProvider = storedUser?.provider as keyof typeof AUTH_PROVIDERS\n\n  // Prefill email if available\n  useEffect(() => {\n    if (typeof userEmail === 'string' && userEmail.length > 0) {\n      setEmail(userEmail)\n    }\n  }, [userEmail])\n\n  const emailOk = useMemo(() => isValidEmail(email || ''), [email])\n  const isValid = useMemo(\n    () => isStrongPassword(password) && emailOk,\n    [password, emailOk],\n  )\n\n  const handleEmailPasswordLogin = async () => {\n    if (!isValid) return\n    try {\n      setIsLoggingIn(true)\n      setErrorMessage(null)\n      await loginWithEmailPassword(email, password, { skipNavigate: true })\n    } catch (e: any) {\n      const msg = typeof e?.message === 'string' ? e.message : 'Login failed.'\n      setErrorMessage(msg)\n    } finally {\n      setIsLoggingIn(false)\n    }\n  }\n\n  // Helper function to format provider names for display\n  const formatProviderName = (provider?: string): string => {\n    if (!provider) return 'Unknown'\n    return (\n      AUTH_PROVIDERS[provider as keyof typeof AUTH_PROVIDERS]?.label ||\n      provider.charAt(0).toUpperCase() + provider.slice(1)\n    )\n  }\n\n  // Render all auth options\n  const renderAllAuthOptions = () => (\n    <>\n      <div className=\"space-y-3 mb-8\">\n        <div className=\"grid grid-cols-2 gap-3\">\n          <AuthButton\n            provider=\"google-oauth2\"\n            onClick={() => handleSocialAuth('google')}\n          />\n          <AuthButton\n            provider=\"microsoft\"\n            onClick={() => handleSocialAuth('microsoft')}\n          />\n        </div>\n\n        <div className=\"grid grid-cols-2 gap-3\">\n          <AuthButton\n            provider=\"apple\"\n            onClick={() => handleSocialAuth('apple')}\n          />\n          <AuthButton\n            provider=\"github\"\n            onClick={() => handleSocialAuth('github')}\n          />\n        </div>\n      </div>\n\n      {/* Email sign in */}\n      <div className=\"space-y-3 mb-8\">\n        <input\n          type=\"email\"\n          placeholder=\"Enter your email\"\n          className=\"w-full h-12 px-3 rounded-md border border-border bg-background text-foreground placeholder:text-muted-foreground\"\n          onChange={e => setEmail(e.target.value)}\n          defaultValue={userEmail}\n        />\n        <AuthButton\n          provider=\"google-oauth2\"\n          onClick={() => {}}\n          className=\"hidden\"\n        />\n        <Button\n          className=\"w-full h-12 text-sm font-medium\"\n          onClick={() => loginWithEmail(email || userEmail)}\n        >\n          Continue with Email\n        </Button>\n      </div>\n\n      {/* Divider */}\n      <div className=\"flex items-center my-8\">\n        <div className=\"flex-1 border-t border-border\"></div>\n        <span className=\"px-4 text-xs text-muted-foreground\">OR</span>\n        <div className=\"flex-1 border-t border-border\"></div>\n      </div>\n\n      {/* Self-hosted option */}\n      <div className=\"space-y-4\">\n        <AuthButton\n          provider=\"self-hosted\"\n          onClick={handleSelfHosted}\n          className=\"w-full\"\n          disabled={!isServerHealthy}\n          title={\n            !isServerHealthy\n              ? 'Local server must be running to use self-hosted option'\n              : undefined\n          }\n        />\n      </div>\n    </>\n  )\n\n  // Render single provider option\n  const renderSingleProviderOption = (\n    provider: keyof typeof AUTH_PROVIDERS,\n  ) => {\n    // Special-case: email provider renders inline email/password form\n    if (provider === 'email') {\n      return (\n        <div className=\"space-y-5\">\n          <div className=\"flex flex-col gap-2\">\n            <label className=\"text-sm text-foreground\">Email</label>\n            <input\n              type=\"email\"\n              placeholder=\"Enter your email\"\n              value={email}\n              onChange={e => setEmail(e.target.value)}\n              className=\"h-10 w-full rounded-md border border-border bg-background px-3 text-foreground placeholder:text-muted-foreground\"\n            />\n          </div>\n\n          <div className=\"flex flex-col gap-2\">\n            <label className=\"text-sm text-foreground\">Password</label>\n            <input\n              type=\"password\"\n              placeholder=\"Enter your password\"\n              value={password}\n              onKeyDown={e => {\n                if (e.key === 'Enter') {\n                  e.preventDefault()\n                  handleEmailPasswordLogin()\n                }\n              }}\n              onChange={e => setPassword(e.target.value)}\n              className=\"h-10 w-full rounded-md border border-border bg-background px-3 text-foreground placeholder:text-muted-foreground\"\n            />\n          </div>\n\n          <Button\n            className=\"h-10 w-full\"\n            disabled={!isValid || isLoggingIn}\n            aria-busy={isLoggingIn}\n            onClick={handleEmailPasswordLogin}\n          >\n            {isLoggingIn && (\n              <span className=\"mr-2 inline-block size-4 rounded-full border-2 border-current border-t-transparent animate-spin\" />\n            )}\n            {isLoggingIn ? 'Logging in…' : 'Log In'}\n          </Button>\n\n          {errorMessage && (\n            <p className=\"mt-2 text-sm text-destructive\">{errorMessage}</p>\n          )}\n        </div>\n      )\n    }\n\n    const getClickHandler = () => {\n      switch (provider) {\n        case 'google-oauth2':\n          return () => handleSocialAuth('google')\n        case 'microsoft':\n          return () => handleSocialAuth('microsoft')\n        case 'apple':\n          return () => handleSocialAuth('apple')\n        case 'github':\n          return () => handleSocialAuth('github')\n        case 'self-hosted':\n          return handleSelfHosted\n        default:\n          return () => console.error('Unknown provider:', provider)\n      }\n    }\n\n    const getLabel = () => {\n      const config = AUTH_PROVIDERS[provider]\n      return `Continue with ${config.label}`\n    }\n\n    return (\n      <div className=\"space-y-4\">\n        <AuthButton\n          provider={provider}\n          onClick={getClickHandler()}\n          className=\"w-full\"\n          disabled={provider === 'self-hosted' && !isServerHealthy}\n          title={\n            provider === 'self-hosted' && !isServerHealthy\n              ? 'Local server must be running to use self-hosted option'\n              : undefined\n          }\n        >\n          {getLabel()}\n        </AuthButton>\n      </div>\n    )\n  }\n\n  // Helper function to render the appropriate auth button based on stored provider\n  const renderAuthButton = () => {\n    // If provider is email, render the inline email/password form\n    if (userProvider === 'email') {\n      return renderSingleProviderOption('email')\n    }\n    if (!userProvider || !AUTH_PROVIDERS[userProvider]) {\n      return renderAllAuthOptions()\n    }\n    return renderSingleProviderOption(userProvider)\n  }\n\n  return (\n    <div className=\"flex h-full w-full bg-background\">\n      {/* Left side - Sign in form */}\n      <div className=\"flex w-[30%] flex-col items-center justify-center px-12 py-12\">\n        <div className=\"w-full max-w-md\">\n          {/* Logo */}\n          <div className=\"mb-8 bg-black rounded-md p-2 w-10 h-10 mx-auto\">\n            <ItoIcon height={24} width={24} style={{ color: '#FFFFFF' }} />\n          </div>\n\n          {/* Title and subtitle */}\n          <div className=\"text-center mb-10\">\n            <h1 className=\"text-3xl font-semibold mb-4 text-foreground\">\n              Welcome back!\n            </h1>\n            <p className=\"text-muted-foreground text-base leading-relaxed\">\n              {userEmail && userProvider\n                ? `You last logged in with ${formatProviderName(userProvider)} (${userEmail})`\n                : userEmail\n                  ? `You last logged in with ${userEmail}`\n                  : 'Sign in to continue with your smart dictation.'}\n            </p>\n          </div>\n\n          {/* Auth buttons - conditionally rendered based on previous provider */}\n          {renderAuthButton()}\n\n          {/* Terms and privacy - only show for self-hosted */}\n          {(userProvider === 'self-hosted' || !userProvider) && (\n            <p className=\"text-xs text-muted-foreground text-center mt-8 leading-relaxed\">\n              Running Ito locally requires additional setup. Please refer to our{' '}\n              <a href=\"#\" className=\"underline\">\n                Github\n              </a>{' '}\n              and{' '}\n              <a href=\"#\" className=\"underline\">\n                Documentation\n              </a>\n            </p>\n          )}\n\n          {/* Link to create new account */}\n          <div className=\"text-center mt-8\">\n            <p className=\"text-sm text-muted-foreground\">\n              {userProvider\n                ? 'Sign in with a different account?'\n                : 'Need to create an account?'}{' '}\n              <button\n                onClick={() => {\n                  // Clear auth to allow selecting a different account, but do not reset onboarding\n                  clearAuth(false)\n                  loadNotes()\n                  loadEntries()\n                  window.location.reload()\n                }}\n                className=\"text-foreground underline font-medium\"\n              >\n                {userProvider ? 'Switch account' : 'Create account'}\n              </button>\n            </p>\n          </div>\n        </div>\n      </div>\n\n      {/* Right side - Placeholder for image */}\n      <div className=\"flex w-[70%] bg-muted/20 items-center justify-center border-l border-border\">\n        <AppOrbitImage />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/welcome/contents/TryItOutContent.tsx",
    "content": "import { Button } from '@/app/components/ui/button'\nimport { useOnboardingStore } from '@/app/store/useOnboardingStore'\nimport { useSettingsStore } from '@/app/store/useSettingsStore'\nimport SlackIcon from '../../icons/SlackIcon'\nimport GmailIcon from '../../icons/GmailIcon'\nimport ChatGPTIcon from '../../icons/ChatGPTIcon'\nimport NotionIcon from '../../icons/NotionIcon'\nimport CursorIcon from '../../icons/CursorIcon'\nimport { useState } from 'react'\nimport { ArrowUp } from '@mynaui/icons-react'\nimport React from 'react'\nimport { ItoMode } from '@/app/generated/ito_pb'\nimport { getKeyDisplay } from '@/app/utils/keyboard'\nimport { usePlatform } from '@/app/hooks/usePlatform'\nimport { KeyName } from '@/lib/types/keyboard'\n\nexport default function TryItOut() {\n  const { decrementOnboardingStep, setOnboardingCompleted } =\n    useOnboardingStore()\n  const { getItoModeShortcuts } = useSettingsStore()\n  const keyboardShortcut = getItoModeShortcuts(ItoMode.TRANSCRIBE)[0].keys\n  const platform = usePlatform()\n  const [selectedApp, setSelectedApp] = useState<\n    'slack' | 'gmail' | 'cursor' | 'chatgpt' | 'notion'\n  >('slack')\n\n  function renderDemo() {\n    if (selectedApp === 'slack') {\n      return (\n        <div className=\"w-[475px] rounded-2xl bg-white shadow-lg flex flex-col gap-4\">\n          <div className=\"flex items-center gap-2 mb-2 bg-neutral-100 py-4 px-4 rounded-t-2xl\">\n            <div\n              className=\"bg-white rounded-md p-1\"\n              style={{ width: 24, height: 24 }}\n            >\n              <SlackIcon />\n            </div>\n            <span className=\"text-base font-medium\">Slack</span>\n          </div>\n          <div className=\"flex items-center gap-2 px-4 mt-24\">\n            <div className=\"w-10 h-10 rounded-md bg-yellow-200 flex items-center justify-center text-lg font-bold\">\n              B\n            </div>\n            <div>\n              <div className=\"font-medium\">Jordan</div>\n              <div className=\"text-sm\">Hey Taylor, is Ito working for you?</div>\n            </div>\n          </div>\n          <div className=\"flex items-center gap-2 px-4 pb-4 rounded-b-2xl\">\n            <input\n              type=\"text\"\n              placeholder={`Hold down on the hotkey(s) and start speaking...`}\n              className=\"w-full h-12 border border-neutral-500 rounded-md px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-0\"\n            />\n          </div>\n        </div>\n      )\n    }\n    if (selectedApp === 'gmail') {\n      return (\n        <div className=\"w-[475px] rounded-2xl bg-white shadow-lg flex flex-col gap-4\">\n          <div className=\"flex items-center gap-2 mb-2 py-4 px-4 rounded-t-2xl border-b border-neutral-200 bg-neutral-100\">\n            <div\n              className=\"bg-white rounded-md p-1\"\n              style={{ width: 24, height: 24 }}\n            >\n              <GmailIcon />\n            </div>\n            <span className=\"text-base font-medium\">Gmail</span>\n          </div>\n          <div className=\"flex flex-col gap-2 px-6 pb-6\">\n            <div className=\"text-sm text-muted-foreground\">\n              Subject:{' '}\n              <span className=\"font-medium text-black\">Quick update</span>\n            </div>\n            <div className=\"border-t border-neutral-200 my-2\" />\n            <textarea\n              placeholder={`Try saying:\\n\\n\"Hi Jordan, wonderful meeting with you today. Do you have any time Monday to follow up on the project? Thanks, Taylor\"`}\n              className=\"w-full resize-none bg-transparent border-none focus:outline-none focus:ring-0 text-sm placeholder:text-muted-foreground\"\n              rows={6}\n            />\n          </div>\n        </div>\n      )\n    }\n    if (selectedApp === 'notion') {\n      return (\n        <div\n          className=\"w-[475px] rounded-2xl bg-white shadow-lg flex flex-col\"\n          style={{ minHeight: 280 }}\n        >\n          <div className=\"flex items-center gap-2 mb-2 py-4 px-4 rounded-t-2xl border-b border-neutral-200 bg-neutral-100\">\n            <div\n              className=\"bg-white rounded-md p-1\"\n              style={{ width: 24, height: 24 }}\n            >\n              <NotionIcon />\n            </div>\n            <span className=\"text-base font-medium\">Notion</span>\n          </div>\n          <div className=\"flex flex-col items-start w-full px-4 py-3\">\n            <span className=\"text-2xl font-bold\">New Note</span>\n            <textarea\n              placeholder={`Try saying: \"Project tasks: Jordan will draft the proposal, Taylor will review and finalize by Friday.\"`}\n              className=\"w-full mt-4 resize-none bg-transparent border-none focus:outline-none focus:ring-0 text-sm placeholder:text-muted-foreground\"\n              rows={4}\n            />\n          </div>\n        </div>\n      )\n    }\n    if (selectedApp === 'chatgpt') {\n      return (\n        <div\n          className=\"w-[475px] rounded-2xl bg-white shadow-lg flex flex-col\"\n          style={{ minHeight: 280 }}\n        >\n          <div className=\"flex items-center gap-2 mb-2 py-4 px-4 rounded-t-2xl border-b border-neutral-200 bg-neutral-100\">\n            <div\n              className=\"bg-white rounded-md p-1\"\n              style={{ width: 24, height: 24 }}\n            >\n              <ChatGPTIcon />\n            </div>\n            <span className=\"text-base font-medium\">ChatGPT</span>\n          </div>\n          <div className=\"flex-1 flex flex-col justify-end px-4 gap-2\">\n            <div className=\"flex-1 flex flex-col justify-end px-6 py-8 gap-2\"></div>\n            <div className=\"flex items-center mb-4 bg-neutral-100 rounded-2xl\">\n              <input\n                type=\"text\"\n                placeholder=\"Ask AI to generate a React component\"\n                className=\"w-full px-4 py-3 bg-transparent border-none focus:outline-none focus:ring-0 text-sm placeholder:text-muted-foreground\"\n              />\n            </div>\n          </div>\n        </div>\n      )\n    }\n    if (selectedApp === 'cursor') {\n      return (\n        <div\n          className=\"w-[475px] rounded-2xl bg-[#23272e] shadow-lg flex flex-col justify-between\"\n          style={{ minHeight: 320 }}\n        >\n          <div className=\"flex flex-col gap-2 p-4 h-full justify-between\">\n            <div>\n              <div className=\"flex items-center gap-2 mb-4 rounded-t-2xl\">\n                <div\n                  className=\"bg-neutral-100 rounded-md p-1\"\n                  style={{ width: 24, height: 24 }}\n                >\n                  <CursorIcon />\n                </div>\n                <span className=\"text-base font-medium text-white\">Cursor</span>\n              </div>\n              <div className=\"flex items-center gap-2 mb-2\">\n                <span className=\"bg-[#23272e] border border-[#3a3f4b] text-xs text-white px-2 py-0.5 rounded font-mono flex items-center gap-1\">\n                  <span className=\"text-[#7dd3fc]\">@</span> TryItOut.tsx\n                </span>\n              </div>\n              <textarea\n                placeholder=\"Plan, search, build anything\"\n                className=\"w-full bg-transparent border-none focus:outline-none focus:ring-0 resize-none text-base text-muted-foreground placeholder:text-muted-foreground mt-2\"\n                rows={3}\n              />\n            </div>\n            <div className=\"flex justify-between gap-2 mt-2\">\n              <div className=\"flex items-center gap-2\">\n                <span className=\"bg-[#23272e] border border-[#3a3f4b] text-xs text-white px-2 py-0.5 rounded flex items-center gap-1\">\n                  <span className=\"text-[#a78bfa]\">∞</span> Agent{' '}\n                  <span className=\"text-[#a3a3a3]\">⌘I</span>\n                </span>\n                <span className=\"bg-[#23272e] border border-[#3a3f4b] text-xs text-white px-2 py-0.5 rounded ml-2\">\n                  Auto <span className=\"text-[#a3a3a3]\">▾</span>\n                </span>\n              </div>\n              <div className=\"flex\">\n                <span className=\"text-[#23272e] p-1 text-lg cursor-pointer rounded-full bg-[#a3a3a3]\">\n                  <ArrowUp size={16} />\n                </span>\n              </div>\n            </div>\n          </div>\n        </div>\n      )\n    }\n    return null\n  }\n\n  const apps = [\n    { key: 'cursor', icon: <CursorIcon /> },\n    { key: 'slack', icon: <SlackIcon /> },\n    { key: 'gmail', icon: <GmailIcon /> },\n    { key: 'chatgpt', icon: <ChatGPTIcon /> },\n    { key: 'notion', icon: <NotionIcon /> },\n  ]\n\n  return (\n    <div className=\"flex flex-row h-full w-full bg-background\">\n      <div className=\"flex flex-col w-[45%] justify-center items-start px-24\">\n        <div className=\"flex flex-col h-full min-h-[400px] justify-between py-12\">\n          <div className=\"mt-8\">\n            <button\n              className=\"mb-4 text-sm text-muted-foreground hover:underline\"\n              type=\"button\"\n              onClick={decrementOnboardingStep}\n            >\n              &lt; Back\n            </button>\n            <h1 className=\"text-3xl mb-4 mt-12\">\n              Use Ito with the keyboard shortcut.\n            </h1>\n            <p className=\"text-base text-muted-foreground mt-6\">\n              Hold down on the{' '}\n              {keyboardShortcut.map((key, idx) => (\n                <React.Fragment key={`keyboard-shortcut-${idx}`}>\n                  <span className=\"inline-flex items-center px-2 py-0.5 bg-neutral-100 border rounded text-xs font-mono mx-1\">\n                    {getKeyDisplay(key, platform, {\n                      showDirectionalText: false,\n                      format: 'label',\n                    })}\n                  </span>\n                  {idx < keyboardShortcut.length - 1 && (\n                    <span className=\"text-muted-foreground\"> + </span>\n                  )}\n                </React.Fragment>\n              ))}{' '}\n              key{keyboardShortcut.length > 1 ? 's' : ''}, speak, and let go to\n              insert spoken text.\n            </p>\n          </div>\n          <div className=\"flex flex-col items-start mb-8\">\n            <Button className=\"w-24\" onClick={setOnboardingCompleted}>\n              Finish\n            </Button>\n          </div>\n        </div>\n      </div>\n      <div className=\"flex w-[55%] items-center justify-center bg-gradient-to-b from-purple-50/10 to-purple-100 border-l-2 border-purple-100\">\n        <div className=\"flex flex-col items-center h-full justify-between pt-36 pb-24\">\n          {renderDemo()}\n          <div className=\"flex flex-col\">\n            <div className=\"flex flex-row gap-2 px-4 pt-3 pb-5 rounded-2xl bg-gray-300/70\">\n              {apps.map(app => (\n                <div\n                  key={app.key}\n                  className=\"relative bg-white p-2 rounded-md shadow-md cursor-pointer flex items-center justify-center\"\n                  style={{ width: 48, height: 48 }}\n                  onClick={() => setSelectedApp(app.key as typeof selectedApp)}\n                >\n                  {app.icon}\n                  {selectedApp === app.key && (\n                    <span className=\"absolute left-1/2 -translate-x-1/2 -bottom-3 w-2 h-2 rounded-full bg-white shadow\" />\n                  )}\n                </div>\n              ))}\n            </div>\n            <div className=\"text-sm text-muted-foreground mt-2 text-center\">\n              Or select any of the apps above\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "app/components/welcome/styles.css",
    "content": ":root {\n  --welcome-c-variant: #8aa6cf;\n}\n\n:root:not(.dark) {\n  --welcome-c-variant: #43679d;\n}\n\n#era-shape {\n  position: relative;\n  width: 250px;\n}\n\n#era-shape > span {\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  transform: translate(-50%, -50%);\n  font-size: 1.05rem;\n  font-weight: bold;\n  opacity: 0.9;\n}\n\n#era-shape svg {\n  padding: 20px;\n  overflow: visible;\n  animation: rotate 20s linear infinite;\n}\n\n#era-shape svg:hover {\n  animation-play-state: paused;\n}\n\n#era-shape svg > path {\n  transform-origin: center;\n  transition: transform 0.4s ease;\n}\n\n#era-shape svg > path:hover {\n  transform: scale(1.15);\n}\n\n@keyframes rotate {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n@keyframes blink {\n  0%,\n  100% {\n    opacity: 1;\n  }\n  50% {\n    opacity: 0;\n  }\n}\n\n.animate-blink {\n  animation: blink 1s steps(1) infinite;\n}\n\n.welcome-content {\n  display: flex;\n  padding: 2rem;\n  gap: 1rem;\n}\n\n.welcome-content h2 {\n  font-size: 1.5rem;\n  font-weight: 600;\n  margin-bottom: 1.5rem;\n  margin-top: 1rem;\n}\n\n.welcome-content p {\n  margin-bottom: 1rem;\n  color: var(--window-c-text);\n  font-size: 15px;\n  font-weight: 400;\n}\n\n.welcome-content-steps {\n  margin-top: 2.5rem;\n}\n\n.welcome-content-step {\n  display: flex;\n  align-items: center;\n  padding: 10px 0;\n}\n.welcome-content-step + .welcome-content-step {\n  border-top: 1px solid #8080802e;\n}\n\n.welcome-content-step h3 {\n  margin-bottom: 2px;\n  color: var(--welcome-c-variant);\n  font-weight: 500;\n  font-size: 15px;\n}\n\n.welcome-content-step p {\n  opacity: 0.8;\n  font-size: 14px;\n  margin: 0;\n  font-weight: 400;\n}\n\n.welcome-content-step svg {\n  width: 20px;\n  height: 20px;\n  fill: var(--welcome-c-variant);\n  /* fill: #8aa6cf; */\n  margin: 0 20px;\n}\n\n.welcome-content .learn-more {\n  margin-top: 2.5rem;\n  font-size: 13px;\n  opacity: 0.7;\n}\n\n.welcome-content .learn-more a {\n  color: var(--welcome-c-variant);\n  text-decoration: none;\n  font-weight: 500;\n}\n"
  },
  {
    "path": "app/components/window/OnboardingTitlebar.tsx",
    "content": "import React from 'react'\nimport {\n  getOnboardingCategoryIndex,\n  useOnboardingStore,\n} from '@/app/store/useOnboardingStore'\n\nexport const OnboardingTitlebar = () => {\n  const { onboardingStep, totalOnboardingSteps, onboardingCategory } =\n    useOnboardingStore()\n  const onboardingProgress = Math.ceil(\n    ((onboardingStep + 1) / totalOnboardingSteps) * 100,\n  )\n  const onboardingCategoryIndex = getOnboardingCategoryIndex(onboardingCategory)\n\n  return (\n    <>\n      {/* Onboarding Steps Text */}\n      <div className=\"onboarding-steps-text\">\n        {['Sign Up', 'Permissions', 'Set Up', 'Try it'].map(\n          (step, idx, arr) => (\n            <React.Fragment key={step}>\n              <span\n                className={`onboarding-step-label${idx <= onboardingCategoryIndex ? ' active' : ''}`}\n              >\n                {step.toUpperCase()}\n              </span>\n              {idx < arr.length - 1 && (\n                <span\n                  className={`onboarding-step-chevron${idx < onboardingCategoryIndex ? ' active' : ''}`}\n                  aria-hidden=\"true\"\n                >\n                  &#8250;\n                </span>\n              )}\n            </React.Fragment>\n          ),\n        )}\n      </div>\n      {/* Onboarding Progress Bar */}\n      <div className=\"onboarding-progress-bar-bg\">\n        <div className=\"onboarding-progress-bar-fg\" />\n      </div>\n      <style>{`\n        .onboarding-steps-text {\n          position: absolute;\n          left: 0;\n          right: 0;\n          top: 0;\n          height: 100%;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          pointer-events: none;\n          z-index: 2;\n          font-size: 14px;\n          font-weight: 500;\n        }\n        .onboarding-step-label {\n          color: #b0b0b0;\n          font-weight: 400;\n          transition: color 0.2s, font-weight 0.2s;\n          display: inline-flex;\n          align-items: center;\n          margin: 0 36px;\n        }\n        .onboarding-step-chevron {\n          color: #d0d0d0;\n          font-size: 24px;\n          margin: 0 36px;\n          margin-top: -4px;\n          display: inline-flex;\n          align-items: center;\n        }\n        .onboarding-step-label.active, .onboarding-step-chevron.active {\n          color: #222;\n          font-weight: 500;\n        }\n        .onboarding-progress-bar-bg {\n          position: absolute;\n          left: 0;\n          right: 0;\n          bottom: 0;\n          height: 3px;\n          background: #ececec;\n          border-radius: 2px;\n          overflow: hidden;\n          pointer-events: none;\n          z-index: 1;\n        }\n        .onboarding-progress-bar-fg {\n          height: 100%;\n          width: ${onboardingProgress}%;\n          background: linear-gradient(90deg, #8aa6cf 0%, #43679d 100%);\n          border-radius: 2px;\n          transition: width 0.4s cubic-bezier(0.4,0,0.2,1);\n        }\n      `}</style>\n    </>\n  )\n}\n"
  },
  {
    "path": "app/components/window/Titlebar.tsx",
    "content": "import { useWindowContext } from './WindowContext'\nimport React, { useState, useEffect } from 'react'\nimport { OnboardingTitlebar } from './OnboardingTitlebar'\nimport { useOnboardingStore } from '@/app/store/useOnboardingStore'\nimport { UserCircle, PanelLeft, CogFour, Logout } from '@mynaui/icons-react'\nimport { useMainStore } from '@/app/store/useMainStore'\nimport { useAuthStore } from '@/app/store/useAuthStore'\nimport { useAuth } from '@/app/components/auth/useAuth'\n\nexport const Titlebar = () => {\n  const { onboardingCompleted } = useOnboardingStore()\n  const { isAuthenticated } = useAuthStore()\n  const showOnboarding = !onboardingCompleted || !isAuthenticated\n  const { toggleNavExpanded, setCurrentPage, setSettingsPage, navExpanded } =\n    useMainStore()\n  const { logoutUser } = useAuth()\n  const wcontext = useWindowContext().window\n  const [showUserDropdown, setShowUserDropdown] = useState(false)\n  const [isUpdateAvailable, setIsUpdateAvailable] = useState(false)\n  const [isUpdateDownloaded, setUpdateDownloaded] = useState(false)\n\n  // Handle clicks outside dropdown to close it\n  useEffect(() => {\n    const handleClickOutside = () => {\n      setShowUserDropdown(false)\n    }\n\n    if (showUserDropdown) {\n      document.addEventListener('click', handleClickOutside)\n      return () => document.removeEventListener('click', handleClickOutside)\n    }\n\n    return () => {}\n  }, [showUserDropdown])\n\n  useEffect(() => {\n    // Check current update status on mount\n    window.api.updater.getUpdateStatus().then(status => {\n      if (status.updateAvailable) {\n        setIsUpdateAvailable(true)\n      }\n      if (status.updateDownloaded) {\n        setUpdateDownloaded(true)\n      }\n    })\n\n    // Listen for future update events\n    window.api.updater.onUpdateAvailable(() => {\n      setIsUpdateAvailable(true)\n    })\n\n    window.api.updater.onUpdateDownloaded(() => {\n      setUpdateDownloaded(true)\n    })\n  }, [])\n\n  const toggleUserDropdown = (e: React.MouseEvent) => {\n    e.stopPropagation()\n    setShowUserDropdown(!showUserDropdown)\n  }\n\n  const handleSettingsClick = (e: React.MouseEvent) => {\n    e.stopPropagation()\n    setCurrentPage('settings')\n    setSettingsPage('account')\n    setShowUserDropdown(false)\n  }\n\n  const handleSignOutClick = async (e: React.MouseEvent) => {\n    e.stopPropagation()\n    try {\n      await logoutUser()\n    } catch (error) {\n      console.error('Logout failed:', error)\n    }\n    setShowUserDropdown(false)\n  }\n\n  // Inline style override for onboarding completed\n  const style: React.CSSProperties = onboardingCompleted\n    ? {\n        position: 'relative' as const,\n        borderBottom: 'none',\n      }\n    : { position: 'relative' as const }\n\n  return (\n    <div\n      className={`window-titlebar ${wcontext?.platform ? `platform-${wcontext.platform}` : ''}`}\n      style={style}\n    >\n      {!showOnboarding && (\n        <div\n          style={{\n            position: 'absolute',\n            left: 0,\n            top: 0,\n            height: '100%',\n            display: 'flex',\n            alignItems: 'center',\n            gap: '2px',\n            zIndex: 10,\n          }}\n        >\n          <div\n            className={`h-full border-r border-neutral-200 transition-all duration-100 ease-in-out ${navExpanded ? 'w-48' : 'w-20'}`}\n          ></div>\n          <div\n            className=\"titlebar-action-btn hover:bg-slate-200 border-l border-neutral-200 ml-2\"\n            style={{\n              display: 'flex',\n              alignItems: 'center',\n              justifyContent: 'center',\n              width: 36,\n              height: 30,\n              border: 'none',\n              cursor: 'pointer',\n              borderRadius: 6,\n              padding: 0,\n            }}\n            aria-label=\"Open Panel\"\n            tabIndex={0}\n            onClick={toggleNavExpanded}\n          >\n            <PanelLeft style={{ width: 20, height: 20 }} />\n          </div>\n        </div>\n      )}\n\n      {showOnboarding && <OnboardingTitlebar />}\n      {wcontext?.platform === 'win32' && <TitlebarControls />}\n\n      {!showOnboarding && (\n        <div\n          style={{\n            position: 'absolute',\n            right: 0,\n            top: 0,\n            height: '100%',\n            display: 'flex',\n            alignItems: 'center',\n            gap: '2px',\n            zIndex: 10,\n          }}\n        >\n          {isUpdateAvailable && (\n            <button\n              className={`titlebar-action-btn bg-sky-800 text-white px-3 py-1 rounded-md font-semibold ${\n                isUpdateDownloaded\n                  ? 'hover:bg-sky-700 cursor-pointer'\n                  : 'cursor-not-allowed opacity-70'\n              }`}\n              disabled={!isUpdateDownloaded}\n              onClick={() => {\n                if (\n                  confirm(\n                    'Are you sure you want to install the update? The app will restart.',\n                  )\n                ) {\n                  window.api.updater.installUpdate()\n                }\n              }}\n            >\n              {isUpdateDownloaded ? 'Install Update' : 'Downloading Update...'}\n            </button>\n          )}\n          <div className=\"relative\">\n            <div\n              className=\"titlebar-action-btn hover:bg-slate-200\"\n              style={{\n                display: 'flex',\n                alignItems: 'center',\n                justifyContent: 'center',\n                width: 36,\n                height: 30,\n                border: 'none',\n                cursor: 'pointer',\n                borderRadius: 6,\n                padding: 0,\n                marginRight: 12,\n              }}\n              aria-label=\"Account\"\n              tabIndex={0}\n              onClick={toggleUserDropdown}\n            >\n              <UserCircle style={{ width: 20, height: 20 }} />\n            </div>\n\n            {/* User Dropdown Menu */}\n            {showUserDropdown && (\n              <div className=\"absolute top-full right-0 mt-1 w-48 bg-white border border-gray-200 rounded-lg shadow-lg z-20\">\n                <button\n                  onClick={handleSettingsClick}\n                  className=\"w-full px-2 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2 rounded-t-lg cursor-pointer\"\n                >\n                  <CogFour className=\"w-4 h-4\" />\n                  Settings\n                </button>\n                <button\n                  onClick={handleSignOutClick}\n                  className=\"w-full px-2 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2 rounded-b-lg cursor-pointer\"\n                >\n                  <Logout className=\"w-4 h-4\" />\n                  Sign Out\n                </button>\n              </div>\n            )}\n          </div>\n        </div>\n      )}\n    </div>\n  )\n}\n\nconst TitlebarControls = () => {\n  const closePath =\n    'M 0,0 0,0.7 4.3,5 0,9.3 0,10 0.7,10 5,5.7 9.3,10 10,10 10,9.3 5.7,5 10,0.7 10,0 9.3,0 5,4.3 0.7,0 Z'\n  const maximizePath = 'M 0,0 0,10 10,10 10,0 Z M 1,1 9,1 9,9 1,9 Z'\n  const minimizePath = 'M 0,5 10,5 10,6 0,6 Z'\n  const wcontext = useWindowContext().window\n\n  return (\n    <div className=\"window-titlebar-controls\">\n      <TitlebarControlButton label=\"close\" svgPath={closePath} />\n      {wcontext?.maximizable && (\n        <TitlebarControlButton label=\"maximize\" svgPath={maximizePath} />\n      )}\n      {wcontext?.minimizable && (\n        <TitlebarControlButton label=\"minimize\" svgPath={minimizePath} />\n      )}\n    </div>\n  )\n}\n\nconst TitlebarControlButton = ({\n  svgPath,\n  label,\n}: {\n  svgPath: string\n  label: string\n}) => {\n  const handleAction = () => {\n    switch (label) {\n      case 'minimize':\n        window.api.invoke('window-minimize')\n        break\n      case 'maximize':\n        window.api.invoke('window-maximize-toggle')\n        break\n      case 'close':\n        window.api.invoke('window-close')\n        break\n      default:\n        console.warn(`Unhandled action for label: ${label}`)\n    }\n  }\n\n  return (\n    <div\n      aria-label={label}\n      className=\"titlebar-controlButton\"\n      onClick={handleAction}\n    >\n      <svg width=\"10\" height=\"10\">\n        <path fill=\"currentColor\" d={svgPath} />\n      </svg>\n    </div>\n  )\n}\n\nexport interface TitlebarProps {\n  title: string\n  titleCentered?: boolean\n  icon?: string\n}\n"
  },
  {
    "path": "app/components/window/TitlebarContext.tsx",
    "content": "import { createContext, useContext } from 'react'\n\nconst TitlebarContext = createContext<TitlebarContextProps | undefined>(\n  undefined,\n)\n\nexport const TitlebarContextProvider = ({\n  children,\n}: {\n  children: React.ReactNode\n}) => {\n  return (\n    <TitlebarContext.Provider value={{}}>{children}</TitlebarContext.Provider>\n  )\n}\n\nexport const useTitlebarContext = () => {\n  const context = useContext(TitlebarContext)\n  if (context === undefined) {\n    throw new Error('useTitlebarContext must be used within a TitlebarContext')\n  }\n  return context\n}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\ninterface TitlebarContextProps {}\n"
  },
  {
    "path": "app/components/window/WindowContext.tsx",
    "content": "import { createContext, useContext, useEffect, useState } from 'react'\nimport { Titlebar, TitlebarProps } from './Titlebar'\nimport { TitlebarContextProvider } from './TitlebarContext'\n\nconst WindowContext = createContext<WindowContextProps | undefined>(undefined)\n\nexport const WindowContextProvider = ({\n  children,\n  titlebar,\n}: WindowContextProviderProps) => {\n  const [initProps, setInitProps] = useState<WindowInitProps | undefined>()\n\n  const defaultTitlebar: TitlebarProps = {\n    title: 'Ito',\n    icon: 'appIcon.png',\n    titleCentered: false,\n  }\n\n  // Merge default titlebar props with user defined props\n  titlebar = { ...defaultTitlebar, ...titlebar }\n\n  useEffect(() => {\n    // Load window init props\n    window.api\n      .invoke('init-window')\n      .then((value: WindowInitProps) => setInitProps(value))\n\n    // Add class to parent element\n    const parent = document.querySelector('.window-content')?.parentElement\n    if (parent) {\n      parent.classList.add('window-frame')\n    }\n  }, [])\n\n  return (\n    <WindowContext.Provider value={{ titlebar, window: initProps! }}>\n      <TitlebarContextProvider>\n        <Titlebar />\n      </TitlebarContextProvider>\n      <WindowContent>{children}</WindowContent>\n    </WindowContext.Provider>\n  )\n}\n\nconst WindowContent = ({ children }: { children: React.ReactNode }) => {\n  return <div className=\"window-content\">{children}</div>\n}\n\nexport const useWindowContext = () => {\n  const context = useContext(WindowContext)\n  if (context === undefined) {\n    throw new Error(\n      'useWindowContext must be used within a WindowContextProvider',\n    )\n  }\n  return context\n}\n\ninterface WindowContextProps {\n  titlebar: TitlebarProps\n  readonly window: WindowInitProps\n}\n\ninterface WindowInitProps {\n  width: number\n  height: number\n  maximizable: boolean\n  minimizable: boolean\n  platform: string\n}\n\ninterface WindowContextProviderProps {\n  children: React.ReactNode\n  titlebar?: TitlebarProps\n}\n"
  },
  {
    "path": "app/generated/buf/validate/validate_pb.ts",
    "content": "// Copyright 2023-2025 Buf Technologies, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//      http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// @generated by protoc-gen-es v2.7.0 with parameter \"target=ts,import_extension=.js\"\n// @generated from file buf/validate/validate.proto (package buf.validate, syntax proto2)\n/* eslint-disable */\n\nimport type { GenEnum, GenExtension, GenFile, GenMessage } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, extDesc, fileDesc, messageDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { Duration, FieldDescriptorProto_Type, FieldOptions, MessageOptions, OneofOptions, Timestamp } from \"@bufbuild/protobuf/wkt\";\nimport { file_google_protobuf_descriptor, file_google_protobuf_duration, file_google_protobuf_timestamp } from \"@bufbuild/protobuf/wkt\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file buf/validate/validate.proto.\n */\nexport const file_buf_validate_validate: GenFile = /*@__PURE__*/\n  fileDesc(\"ChtidWYvdmFsaWRhdGUvdmFsaWRhdGUucHJvdG8SDGJ1Zi52YWxpZGF0ZSI3CgRSdWxlEgoKAmlkGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSEgoKZXhwcmVzc2lvbhgDIAEoCSJuCgxNZXNzYWdlUnVsZXMSHwoDY2VsGAMgAygLMhIuYnVmLnZhbGlkYXRlLlJ1bGUSLQoFb25lb2YYBCADKAsyHi5idWYudmFsaWRhdGUuTWVzc2FnZU9uZW9mUnVsZUoECAEQAlIIZGlzYWJsZWQiNAoQTWVzc2FnZU9uZW9mUnVsZRIOCgZmaWVsZHMYASADKAkSEAoIcmVxdWlyZWQYAiABKAgiHgoKT25lb2ZSdWxlcxIQCghyZXF1aXJlZBgBIAEoCCK/CAoKRmllbGRSdWxlcxIfCgNjZWwYFyADKAsyEi5idWYudmFsaWRhdGUuUnVsZRIQCghyZXF1aXJlZBgZIAEoCBIkCgZpZ25vcmUYGyABKA4yFC5idWYudmFsaWRhdGUuSWdub3JlEikKBWZsb2F0GAEgASgLMhguYnVmLnZhbGlkYXRlLkZsb2F0UnVsZXNIABIrCgZkb3VibGUYAiABKAsyGS5idWYudmFsaWRhdGUuRG91YmxlUnVsZXNIABIpCgVpbnQzMhgDIAEoCzIYLmJ1Zi52YWxpZGF0ZS5JbnQzMlJ1bGVzSAASKQoFaW50NjQYBCABKAsyGC5idWYudmFsaWRhdGUuSW50NjRSdWxlc0gAEisKBnVpbnQzMhgFIAEoCzIZLmJ1Zi52YWxpZGF0ZS5VSW50MzJSdWxlc0gAEisKBnVpbnQ2NBgGIAEoCzIZLmJ1Zi52YWxpZGF0ZS5VSW50NjRSdWxlc0gAEisKBnNpbnQzMhgHIAEoCzIZLmJ1Zi52YWxpZGF0ZS5TSW50MzJSdWxlc0gAEisKBnNpbnQ2NBgIIAEoCzIZLmJ1Zi52YWxpZGF0ZS5TSW50NjRSdWxlc0gAEi0KB2ZpeGVkMzIYCSABKAsyGi5idWYudmFsaWRhdGUuRml4ZWQzMlJ1bGVzSAASLQoHZml4ZWQ2NBgKIAEoCzIaLmJ1Zi52YWxpZGF0ZS5GaXhlZDY0UnVsZXNIABIvCghzZml4ZWQzMhgLIAEoCzIbLmJ1Zi52YWxpZGF0ZS5TRml4ZWQzMlJ1bGVzSAASLwoIc2ZpeGVkNjQYDCABKAsyGy5idWYudmFsaWRhdGUuU0ZpeGVkNjRSdWxlc0gAEicKBGJvb2wYDSABKAsyFy5idWYudmFsaWRhdGUuQm9vbFJ1bGVzSAASKwoGc3RyaW5nGA4gASgLMhkuYnVmLnZhbGlkYXRlLlN0cmluZ1J1bGVzSAASKQoFYnl0ZXMYDyABKAsyGC5idWYudmFsaWRhdGUuQnl0ZXNSdWxlc0gAEicKBGVudW0YECABKAsyFy5idWYudmFsaWRhdGUuRW51bVJ1bGVzSAASLwoIcmVwZWF0ZWQYEiABKAsyGy5idWYudmFsaWRhdGUuUmVwZWF0ZWRSdWxlc0gAEiUKA21hcBgTIAEoCzIWLmJ1Zi52YWxpZGF0ZS5NYXBSdWxlc0gAEiUKA2FueRgUIAEoCzIWLmJ1Zi52YWxpZGF0ZS5BbnlSdWxlc0gAEi8KCGR1cmF0aW9uGBUgASgLMhsuYnVmLnZhbGlkYXRlLkR1cmF0aW9uUnVsZXNIABIxCgl0aW1lc3RhbXAYFiABKAsyHC5idWYudmFsaWRhdGUuVGltZXN0YW1wUnVsZXNIAEIGCgR0eXBlSgQIGBAZSgQIGhAbUgdza2lwcGVkUgxpZ25vcmVfZW1wdHkiVQoPUHJlZGVmaW5lZFJ1bGVzEh8KA2NlbBgBIAMoCzISLmJ1Zi52YWxpZGF0ZS5SdWxlSgQIGBAZSgQIGhAbUgdza2lwcGVkUgxpZ25vcmVfZW1wdHki2hcKCkZsb2F0UnVsZXMSgwEKBWNvbnN0GAEgASgCQnTCSHEKbwoLZmxvYXQuY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKfAQoCbHQYAiABKAJCkAHCSIwBCokBCghmbG9hdC5sdBp9IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA+PSBydWxlcy5sdCk/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKvAQoDbHRlGAMgASgCQp8BwkibAQqYAQoJZmxvYXQubHRlGooBIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA+IHJ1bGVzLmx0ZSk/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAAS7wcKAmd0GAQgASgCQuAHwkjcBwqNAQoIZmxvYXQuZ3QagAEhaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwrDAQoLZmxvYXQuZ3RfbHQaswFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrNAQoVZmxvYXQuZ3RfbHRfZXhjbHVzaXZlGrMBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmICh0aGlzLmlzTmFuKCkgfHwgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCkpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycK0wEKDGZsb2F0Lmd0X2x0ZRrCAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCt0BChZmbG9hdC5ndF9sdGVfZXhjbHVzaXZlGsIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHRoaXMuaXNOYW4oKSB8fCAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARK6CAoDZ3RlGAUgASgCQqoIwkimCAqbAQoJZmxvYXQuZ3RlGo0BIWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCtIBCgxmbG9hdC5ndGVfbHQawQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtwBChZmbG9hdC5ndGVfbHRfZXhjbHVzaXZlGsEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ZSAmJiAodGhpcy5pc05hbigpIHx8IChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwriAQoNZmxvYXQuZ3RlX2x0ZRrQAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJycK7AEKF2Zsb2F0Lmd0ZV9sdGVfZXhjbHVzaXZlGtABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmICh0aGlzLmlzTmFuKCkgfHwgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSkpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEn8KAmluGAYgAygCQnPCSHAKbgoIZmxvYXQuaW4aYiEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ3ZhbHVlIG11c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEnYKBm5vdF9pbhgHIAMoAkJmwkhjCmEKDGZsb2F0Lm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEnUKBmZpbml0ZRgIIAEoCEJlwkhiCmAKDGZsb2F0LmZpbml0ZRpQcnVsZXMuZmluaXRlID8gKHRoaXMuaXNOYW4oKSB8fCB0aGlzLmlzSW5mKCkgPyAndmFsdWUgbXVzdCBiZSBmaW5pdGUnIDogJycpIDogJycSKwoHZXhhbXBsZRgJIAMoAkIawkgXChUKDWZsb2F0LmV4YW1wbGUaBHRydWUqCQjoBxCAgICAAkILCglsZXNzX3RoYW5CDgoMZ3JlYXRlcl90aGFuIu0XCgtEb3VibGVSdWxlcxKEAQoFY29uc3QYASABKAFCdcJIcgpwCgxkb3VibGUuY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKgAQoCbHQYAiABKAFCkQHCSI0BCooBCglkb3VibGUubHQafSFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPj0gcnVsZXMubHQpPyAndmFsdWUgbXVzdCBiZSBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMubHRdKSA6ICcnSAASsAEKA2x0ZRgDIAEoAUKgAcJInAEKmQEKCmRvdWJsZS5sdGUaigEhaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzID4gcnVsZXMubHRlKT8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABL0BwoCZ3QYBCABKAFC5QfCSOEHCo4BCglkb3VibGUuZ3QagAEhaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwrEAQoMZG91YmxlLmd0X2x0GrMBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndCAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKzgEKFmRvdWJsZS5ndF9sdF9leGNsdXNpdmUaswFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHRoaXMuaXNOYW4oKSB8fCAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrUAQoNZG91YmxlLmd0X2x0ZRrCAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCt4BChdkb3VibGUuZ3RfbHRlX2V4Y2x1c2l2ZRrCAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmICh0aGlzLmlzTmFuKCkgfHwgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCkpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAESvwgKA2d0ZRgFIAEoAUKvCMJIqwgKnAEKCmRvdWJsZS5ndGUajQEhaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZV0pIDogJycK0wEKDWRvdWJsZS5ndGVfbHQawQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCt0BChdkb3VibGUuZ3RlX2x0X2V4Y2x1c2l2ZRrBAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHRoaXMuaXNOYW4oKSB8fCAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycK4wEKDmRvdWJsZS5ndGVfbHRlGtABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrtAQoYZG91YmxlLmd0ZV9sdGVfZXhjbHVzaXZlGtABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmICh0aGlzLmlzTmFuKCkgfHwgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSkpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEoABCgJpbhgGIAMoAUJ0wkhxCm8KCWRvdWJsZS5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSdwoGbm90X2luGAcgAygBQmfCSGQKYgoNZG91YmxlLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEnYKBmZpbml0ZRgIIAEoCEJmwkhjCmEKDWRvdWJsZS5maW5pdGUaUHJ1bGVzLmZpbml0ZSA/ICh0aGlzLmlzTmFuKCkgfHwgdGhpcy5pc0luZigpID8gJ3ZhbHVlIG11c3QgYmUgZmluaXRlJyA6ICcnKSA6ICcnEiwKB2V4YW1wbGUYCSADKAFCG8JIGAoWCg5kb3VibGUuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4ijBUKCkludDMyUnVsZXMSgwEKBWNvbnN0GAEgASgFQnTCSHEKbwoLaW50MzIuY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKKAQoCbHQYAiABKAVCfMJIeQp3CghpbnQzMi5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKcAQoDbHRlGAMgASgFQowBwkiIAQqFAQoJaW50MzIubHRlGnghaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+IHJ1bGVzLmx0ZT8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABKXBwoCZ3QYBCABKAVCiAfCSIQHCnoKCGludDMyLmd0Gm4haGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwqzAQoLaW50MzIuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrsBChVpbnQzMi5ndF9sdF9leGNsdXNpdmUaoQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrDAQoMaW50MzIuZ3RfbHRlGrIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrLAQoWaW50MzIuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES4wcKA2d0ZRgFIAEoBULTB8JIzwcKiAEKCWludDMyLmd0ZRp7IWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPCBydWxlcy5ndGU/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCsIBCgxpbnQzMi5ndGVfbHQasQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKygEKFmludDMyLmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtIBCg1pbnQzMi5ndGVfbHRlGsABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnCtoBChdpbnQzMi5ndGVfbHRlX2V4Y2x1c2l2ZRq+AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARJ/CgJpbhgGIAMoBUJzwkhwCm4KCGludDMyLmluGmIhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ2CgZub3RfaW4YByADKAVCZsJIYwphCgxpbnQzMi5ub3RfaW4aUXRoaXMgaW4gcnVsZXMubm90X2luID8gJ3ZhbHVlIG11c3Qgbm90IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbcnVsZXMubm90X2luXSkgOiAnJxIrCgdleGFtcGxlGAggAygFQhrCSBcKFQoNaW50MzIuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4ijBUKCkludDY0UnVsZXMSgwEKBWNvbnN0GAEgASgDQnTCSHEKbwoLaW50NjQuY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKKAQoCbHQYAiABKANCfMJIeQp3CghpbnQ2NC5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKcAQoDbHRlGAMgASgDQowBwkiIAQqFAQoJaW50NjQubHRlGnghaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+IHJ1bGVzLmx0ZT8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABKXBwoCZ3QYBCABKANCiAfCSIQHCnoKCGludDY0Lmd0Gm4haGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwqzAQoLaW50NjQuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrsBChVpbnQ2NC5ndF9sdF9leGNsdXNpdmUaoQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrDAQoMaW50NjQuZ3RfbHRlGrIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrLAQoWaW50NjQuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES4wcKA2d0ZRgFIAEoA0LTB8JIzwcKiAEKCWludDY0Lmd0ZRp7IWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPCBydWxlcy5ndGU/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCsIBCgxpbnQ2NC5ndGVfbHQasQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKygEKFmludDY0Lmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtIBCg1pbnQ2NC5ndGVfbHRlGsABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnCtoBChdpbnQ2NC5ndGVfbHRlX2V4Y2x1c2l2ZRq+AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARJ/CgJpbhgGIAMoA0JzwkhwCm4KCGludDY0LmluGmIhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ2CgZub3RfaW4YByADKANCZsJIYwphCgxpbnQ2NC5ub3RfaW4aUXRoaXMgaW4gcnVsZXMubm90X2luID8gJ3ZhbHVlIG11c3Qgbm90IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbcnVsZXMubm90X2luXSkgOiAnJxIrCgdleGFtcGxlGAkgAygDQhrCSBcKFQoNaW50NjQuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4inhUKC1VJbnQzMlJ1bGVzEoQBCgVjb25zdBgBIAEoDUJ1wkhyCnAKDHVpbnQzMi5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEosBCgJsdBgCIAEoDUJ9wkh6CngKCXVpbnQzMi5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKdAQoDbHRlGAMgASgNQo0BwkiJAQqGAQoKdWludDMyLmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASnAcKAmd0GAQgASgNQo0HwkiJBwp7Cgl1aW50MzIuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrQBCgx1aW50MzIuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrwBChZ1aW50MzIuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxAEKDXVpbnQzMi5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCswBChd1aW50MzIuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES6AcKA2d0ZRgFIAEoDULYB8JI1AcKiQEKCnVpbnQzMi5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrDAQoNdWludDMyLmd0ZV9sdBqxAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrLAQoXdWludDMyLmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtMBCg51aW50MzIuZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrbAQoYdWludDMyLmd0ZV9sdGVfZXhjbHVzaXZlGr4BaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEoABCgJpbhgGIAMoDUJ0wkhxCm8KCXVpbnQzMi5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSdwoGbm90X2luGAcgAygNQmfCSGQKYgoNdWludDMyLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEiwKB2V4YW1wbGUYCCADKA1CG8JIGAoWCg51aW50MzIuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4inhUKC1VJbnQ2NFJ1bGVzEoQBCgVjb25zdBgBIAEoBEJ1wkhyCnAKDHVpbnQ2NC5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEosBCgJsdBgCIAEoBEJ9wkh6CngKCXVpbnQ2NC5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKdAQoDbHRlGAMgASgEQo0BwkiJAQqGAQoKdWludDY0Lmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASnAcKAmd0GAQgASgEQo0HwkiJBwp7Cgl1aW50NjQuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrQBCgx1aW50NjQuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrwBChZ1aW50NjQuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxAEKDXVpbnQ2NC5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCswBChd1aW50NjQuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES6AcKA2d0ZRgFIAEoBELYB8JI1AcKiQEKCnVpbnQ2NC5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrDAQoNdWludDY0Lmd0ZV9sdBqxAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrLAQoXdWludDY0Lmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtMBCg51aW50NjQuZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrbAQoYdWludDY0Lmd0ZV9sdGVfZXhjbHVzaXZlGr4BaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEoABCgJpbhgGIAMoBEJ0wkhxCm8KCXVpbnQ2NC5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSdwoGbm90X2luGAcgAygEQmfCSGQKYgoNdWludDY0Lm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEiwKB2V4YW1wbGUYCCADKARCG8JIGAoWCg51aW50NjQuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4inhUKC1NJbnQzMlJ1bGVzEoQBCgVjb25zdBgBIAEoEUJ1wkhyCnAKDHNpbnQzMi5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEosBCgJsdBgCIAEoEUJ9wkh6CngKCXNpbnQzMi5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKdAQoDbHRlGAMgASgRQo0BwkiJAQqGAQoKc2ludDMyLmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASnAcKAmd0GAQgASgRQo0HwkiJBwp7CglzaW50MzIuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrQBCgxzaW50MzIuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrwBChZzaW50MzIuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxAEKDXNpbnQzMi5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCswBChdzaW50MzIuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES6AcKA2d0ZRgFIAEoEULYB8JI1AcKiQEKCnNpbnQzMi5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrDAQoNc2ludDMyLmd0ZV9sdBqxAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrLAQoXc2ludDMyLmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtMBCg5zaW50MzIuZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrbAQoYc2ludDMyLmd0ZV9sdGVfZXhjbHVzaXZlGr4BaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEoABCgJpbhgGIAMoEUJ0wkhxCm8KCXNpbnQzMi5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSdwoGbm90X2luGAcgAygRQmfCSGQKYgoNc2ludDMyLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEiwKB2V4YW1wbGUYCCADKBFCG8JIGAoWCg5zaW50MzIuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4inhUKC1NJbnQ2NFJ1bGVzEoQBCgVjb25zdBgBIAEoEkJ1wkhyCnAKDHNpbnQ2NC5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEosBCgJsdBgCIAEoEkJ9wkh6CngKCXNpbnQ2NC5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKdAQoDbHRlGAMgASgSQo0BwkiJAQqGAQoKc2ludDY0Lmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASnAcKAmd0GAQgASgSQo0HwkiJBwp7CglzaW50NjQuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrQBCgxzaW50NjQuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrwBChZzaW50NjQuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxAEKDXNpbnQ2NC5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCswBChdzaW50NjQuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES6AcKA2d0ZRgFIAEoEkLYB8JI1AcKiQEKCnNpbnQ2NC5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrDAQoNc2ludDY0Lmd0ZV9sdBqxAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrLAQoXc2ludDY0Lmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtMBCg5zaW50NjQuZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrbAQoYc2ludDY0Lmd0ZV9sdGVfZXhjbHVzaXZlGr4BaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEoABCgJpbhgGIAMoEkJ0wkhxCm8KCXNpbnQ2NC5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSdwoGbm90X2luGAcgAygSQmfCSGQKYgoNc2ludDY0Lm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEiwKB2V4YW1wbGUYCCADKBJCG8JIGAoWCg5zaW50NjQuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4irxUKDEZpeGVkMzJSdWxlcxKFAQoFY29uc3QYASABKAdCdsJIcwpxCg1maXhlZDMyLmNvbnN0GmB0aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICd2YWx1ZSBtdXN0IGVxdWFsICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSjAEKAmx0GAIgASgHQn7CSHsKeQoKZml4ZWQzMi5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKeAQoDbHRlGAMgASgHQo4BwkiKAQqHAQoLZml4ZWQzMi5sdGUaeCFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID4gcnVsZXMubHRlPyAndmFsdWUgbXVzdCBiZSBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMubHRlXSkgOiAnJ0gAEqEHCgJndBgEIAEoB0KSB8JIjgcKfAoKZml4ZWQzMi5ndBpuIWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPD0gcnVsZXMuZ3Q/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndF0pIDogJycKtQEKDWZpeGVkMzIuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCr0BChdmaXhlZDMyLmd0X2x0X2V4Y2x1c2l2ZRqhAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndCAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCsUBCg5maXhlZDMyLmd0X2x0ZRqyAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJycKzQEKGGZpeGVkMzIuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES7QcKA2d0ZRgFIAEoB0LdB8JI2QcKigEKC2ZpeGVkMzIuZ3RlGnshaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8IHJ1bGVzLmd0ZT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZV0pIDogJycKxAEKDmZpeGVkMzIuZ3RlX2x0GrEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCswBChhmaXhlZDMyLmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtQBCg9maXhlZDMyLmd0ZV9sdGUawAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3RlICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJycK3AEKGWZpeGVkMzIuZ3RlX2x0ZV9leGNsdXNpdmUavgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnSAESgQEKAmluGAYgAygHQnXCSHIKcAoKZml4ZWQzMi5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSeAoGbm90X2luGAcgAygHQmjCSGUKYwoOZml4ZWQzMi5ub3RfaW4aUXRoaXMgaW4gcnVsZXMubm90X2luID8gJ3ZhbHVlIG11c3Qgbm90IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbcnVsZXMubm90X2luXSkgOiAnJxItCgdleGFtcGxlGAggAygHQhzCSBkKFwoPZml4ZWQzMi5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiKvFQoMRml4ZWQ2NFJ1bGVzEoUBCgVjb25zdBgBIAEoBkJ2wkhzCnEKDWZpeGVkNjQuY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKMAQoCbHQYAiABKAZCfsJIewp5CgpmaXhlZDY0Lmx0GmshaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAEp4BCgNsdGUYAyABKAZCjgHCSIoBCocBCgtmaXhlZDY0Lmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASoQcKAmd0GAQgASgGQpIHwkiOBwp8CgpmaXhlZDY0Lmd0Gm4haGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwq1AQoNZml4ZWQ2NC5ndF9sdBqjAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKvQEKF2ZpeGVkNjQuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxQEKDmZpeGVkNjQuZ3RfbHRlGrIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrNAQoYZml4ZWQ2NC5ndF9sdGVfZXhjbHVzaXZlGrABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARLtBwoDZ3RlGAUgASgGQt0HwkjZBwqKAQoLZml4ZWQ2NC5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrEAQoOZml4ZWQ2NC5ndGVfbHQasQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKzAEKGGZpeGVkNjQuZ3RlX2x0X2V4Y2x1c2l2ZRqvAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycK1AEKD2ZpeGVkNjQuZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrcAQoZZml4ZWQ2NC5ndGVfbHRlX2V4Y2x1c2l2ZRq+AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARKBAQoCaW4YBiADKAZCdcJIcgpwCgpmaXhlZDY0LmluGmIhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ4CgZub3RfaW4YByADKAZCaMJIZQpjCg5maXhlZDY0Lm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEi0KB2V4YW1wbGUYCCADKAZCHMJIGQoXCg9maXhlZDY0LmV4YW1wbGUaBHRydWUqCQjoBxCAgICAAkILCglsZXNzX3RoYW5CDgoMZ3JlYXRlcl90aGFuIsAVCg1TRml4ZWQzMlJ1bGVzEoYBCgVjb25zdBgBIAEoD0J3wkh0CnIKDnNmaXhlZDMyLmNvbnN0GmB0aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICd2YWx1ZSBtdXN0IGVxdWFsICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSjQEKAmx0GAIgASgPQn/CSHwKegoLc2ZpeGVkMzIubHQaayFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID49IHJ1bGVzLmx0PyAndmFsdWUgbXVzdCBiZSBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMubHRdKSA6ICcnSAASnwEKA2x0ZRgDIAEoD0KPAcJIiwEKiAEKDHNmaXhlZDMyLmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASpgcKAmd0GAQgASgPQpcHwkiTBwp9CgtzZml4ZWQzMi5ndBpuIWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPD0gcnVsZXMuZ3Q/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndF0pIDogJycKtgEKDnNmaXhlZDMyLmd0X2x0GqMBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndCAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwq+AQoYc2ZpeGVkMzIuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxgEKD3NmaXhlZDMyLmd0X2x0ZRqyAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJycKzgEKGXNmaXhlZDMyLmd0X2x0ZV9leGNsdXNpdmUasAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndCAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJ0gBEvIHCgNndGUYBSABKA9C4gfCSN4HCosBCgxzZml4ZWQzMi5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrFAQoPc2ZpeGVkMzIuZ3RlX2x0GrEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCs0BChlzZml4ZWQzMi5ndGVfbHRfZXhjbHVzaXZlGq8BaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrVAQoQc2ZpeGVkMzIuZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrdAQoac2ZpeGVkMzIuZ3RlX2x0ZV9leGNsdXNpdmUavgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnSAESggEKAmluGAYgAygPQnbCSHMKcQoLc2ZpeGVkMzIuaW4aYiEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ3ZhbHVlIG11c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEnkKBm5vdF9pbhgHIAMoD0JpwkhmCmQKD3NmaXhlZDMyLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEi4KB2V4YW1wbGUYCCADKA9CHcJIGgoYChBzZml4ZWQzMi5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiLAFQoNU0ZpeGVkNjRSdWxlcxKGAQoFY29uc3QYASABKBBCd8JIdApyCg5zZml4ZWQ2NC5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEo0BCgJsdBgCIAEoEEJ/wkh8CnoKC3NmaXhlZDY0Lmx0GmshaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAEp8BCgNsdGUYAyABKBBCjwHCSIsBCogBCgxzZml4ZWQ2NC5sdGUaeCFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID4gcnVsZXMubHRlPyAndmFsdWUgbXVzdCBiZSBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMubHRlXSkgOiAnJ0gAEqYHCgJndBgEIAEoEEKXB8JIkwcKfQoLc2ZpeGVkNjQuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrYBCg5zZml4ZWQ2NC5ndF9sdBqjAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKvgEKGHNmaXhlZDY0Lmd0X2x0X2V4Y2x1c2l2ZRqhAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndCAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCsYBCg9zZml4ZWQ2NC5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCs4BChlzZml4ZWQ2NC5ndF9sdGVfZXhjbHVzaXZlGrABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARLyBwoDZ3RlGAUgASgQQuIHwkjeBwqLAQoMc2ZpeGVkNjQuZ3RlGnshaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8IHJ1bGVzLmd0ZT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZV0pIDogJycKxQEKD3NmaXhlZDY0Lmd0ZV9sdBqxAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrNAQoZc2ZpeGVkNjQuZ3RlX2x0X2V4Y2x1c2l2ZRqvAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycK1QEKEHNmaXhlZDY0Lmd0ZV9sdGUawAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3RlICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJycK3QEKGnNmaXhlZDY0Lmd0ZV9sdGVfZXhjbHVzaXZlGr4BaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEoIBCgJpbhgGIAMoEEJ2wkhzCnEKC3NmaXhlZDY0LmluGmIhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ5CgZub3RfaW4YByADKBBCacJIZgpkCg9zZml4ZWQ2NC5ub3RfaW4aUXRoaXMgaW4gcnVsZXMubm90X2luID8gJ3ZhbHVlIG11c3Qgbm90IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbcnVsZXMubm90X2luXSkgOiAnJxIuCgdleGFtcGxlGAggAygQQh3CSBoKGAoQc2ZpeGVkNjQuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4ixwEKCUJvb2xSdWxlcxKCAQoFY29uc3QYASABKAhCc8JIcApuCgpib29sLmNvbnN0GmB0aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICd2YWx1ZSBtdXN0IGVxdWFsICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSKgoHZXhhbXBsZRgCIAMoCEIZwkgWChQKDGJvb2wuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACIpA3CgtTdHJpbmdSdWxlcxKGAQoFY29uc3QYASABKAlCd8JIdApyCgxzdHJpbmcuY29uc3QaYnRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgYCVzYCcuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEn4KA2xlbhgTIAEoBEJxwkhuCmwKCnN0cmluZy5sZW4aXnVpbnQodGhpcy5zaXplKCkpICE9IHJ1bGVzLmxlbiA/ICd2YWx1ZSBsZW5ndGggbXVzdCBiZSAlcyBjaGFyYWN0ZXJzJy5mb3JtYXQoW3J1bGVzLmxlbl0pIDogJycSmQEKB21pbl9sZW4YAiABKARChwHCSIMBCoABCg5zdHJpbmcubWluX2xlbhpudWludCh0aGlzLnNpemUoKSkgPCBydWxlcy5taW5fbGVuID8gJ3ZhbHVlIGxlbmd0aCBtdXN0IGJlIGF0IGxlYXN0ICVzIGNoYXJhY3RlcnMnLmZvcm1hdChbcnVsZXMubWluX2xlbl0pIDogJycSlwEKB21heF9sZW4YAyABKARChQHCSIEBCn8KDnN0cmluZy5tYXhfbGVuGm11aW50KHRoaXMuc2l6ZSgpKSA+IHJ1bGVzLm1heF9sZW4gPyAndmFsdWUgbGVuZ3RoIG11c3QgYmUgYXQgbW9zdCAlcyBjaGFyYWN0ZXJzJy5mb3JtYXQoW3J1bGVzLm1heF9sZW5dKSA6ICcnEpsBCglsZW5fYnl0ZXMYFCABKARChwHCSIMBCoABChBzdHJpbmcubGVuX2J5dGVzGmx1aW50KGJ5dGVzKHRoaXMpLnNpemUoKSkgIT0gcnVsZXMubGVuX2J5dGVzID8gJ3ZhbHVlIGxlbmd0aCBtdXN0IGJlICVzIGJ5dGVzJy5mb3JtYXQoW3J1bGVzLmxlbl9ieXRlc10pIDogJycSowEKCW1pbl9ieXRlcxgEIAEoBEKPAcJIiwEKiAEKEHN0cmluZy5taW5fYnl0ZXMadHVpbnQoYnl0ZXModGhpcykuc2l6ZSgpKSA8IHJ1bGVzLm1pbl9ieXRlcyA/ICd2YWx1ZSBsZW5ndGggbXVzdCBiZSBhdCBsZWFzdCAlcyBieXRlcycuZm9ybWF0KFtydWxlcy5taW5fYnl0ZXNdKSA6ICcnEqIBCgltYXhfYnl0ZXMYBSABKARCjgHCSIoBCocBChBzdHJpbmcubWF4X2J5dGVzGnN1aW50KGJ5dGVzKHRoaXMpLnNpemUoKSkgPiBydWxlcy5tYXhfYnl0ZXMgPyAndmFsdWUgbGVuZ3RoIG11c3QgYmUgYXQgbW9zdCAlcyBieXRlcycuZm9ybWF0KFtydWxlcy5tYXhfYnl0ZXNdKSA6ICcnEo0BCgdwYXR0ZXJuGAYgASgJQnzCSHkKdwoOc3RyaW5nLnBhdHRlcm4aZSF0aGlzLm1hdGNoZXMocnVsZXMucGF0dGVybikgPyAndmFsdWUgZG9lcyBub3QgbWF0Y2ggcmVnZXggcGF0dGVybiBgJXNgJy5mb3JtYXQoW3J1bGVzLnBhdHRlcm5dKSA6ICcnEoQBCgZwcmVmaXgYByABKAlCdMJIcQpvCg1zdHJpbmcucHJlZml4Gl4hdGhpcy5zdGFydHNXaXRoKHJ1bGVzLnByZWZpeCkgPyAndmFsdWUgZG9lcyBub3QgaGF2ZSBwcmVmaXggYCVzYCcuZm9ybWF0KFtydWxlcy5wcmVmaXhdKSA6ICcnEoIBCgZzdWZmaXgYCCABKAlCcsJIbwptCg1zdHJpbmcuc3VmZml4GlwhdGhpcy5lbmRzV2l0aChydWxlcy5zdWZmaXgpID8gJ3ZhbHVlIGRvZXMgbm90IGhhdmUgc3VmZml4IGAlc2AnLmZvcm1hdChbcnVsZXMuc3VmZml4XSkgOiAnJxKQAQoIY29udGFpbnMYCSABKAlCfsJIewp5Cg9zdHJpbmcuY29udGFpbnMaZiF0aGlzLmNvbnRhaW5zKHJ1bGVzLmNvbnRhaW5zKSA/ICd2YWx1ZSBkb2VzIG5vdCBjb250YWluIHN1YnN0cmluZyBgJXNgJy5mb3JtYXQoW3J1bGVzLmNvbnRhaW5zXSkgOiAnJxKYAQoMbm90X2NvbnRhaW5zGBcgASgJQoEBwkh+CnwKE3N0cmluZy5ub3RfY29udGFpbnMaZXRoaXMuY29udGFpbnMocnVsZXMubm90X2NvbnRhaW5zKSA/ICd2YWx1ZSBjb250YWlucyBzdWJzdHJpbmcgYCVzYCcuZm9ybWF0KFtydWxlcy5ub3RfY29udGFpbnNdKSA6ICcnEoABCgJpbhgKIAMoCUJ0wkhxCm8KCXN0cmluZy5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSdwoGbm90X2luGAsgAygJQmfCSGQKYgoNc3RyaW5nLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEt8BCgVlbWFpbBgMIAEoCELNAcJIyQEKYQoMc3RyaW5nLmVtYWlsEiN2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgZW1haWwgYWRkcmVzcxosIXJ1bGVzLmVtYWlsIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5pc0VtYWlsKCkKZAoSc3RyaW5nLmVtYWlsX2VtcHR5EjJ2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgZW1haWwgYWRkcmVzcxoaIXJ1bGVzLmVtYWlsIHx8IHRoaXMgIT0gJydIABLnAQoIaG9zdG5hbWUYDSABKAhC0gHCSM4BCmUKD3N0cmluZy5ob3N0bmFtZRIedmFsdWUgbXVzdCBiZSBhIHZhbGlkIGhvc3RuYW1lGjIhcnVsZXMuaG9zdG5hbWUgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSG9zdG5hbWUoKQplChVzdHJpbmcuaG9zdG5hbWVfZW1wdHkSLXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBob3N0bmFtZRodIXJ1bGVzLmhvc3RuYW1lIHx8IHRoaXMgIT0gJydIABLHAQoCaXAYDiABKAhCuAHCSLQBClUKCXN0cmluZy5pcBIgdmFsdWUgbXVzdCBiZSBhIHZhbGlkIElQIGFkZHJlc3MaJiFydWxlcy5pcCB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNJcCgpClsKD3N0cmluZy5pcF9lbXB0eRIvdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQIGFkZHJlc3MaFyFydWxlcy5pcCB8fCB0aGlzICE9ICcnSAAS1gEKBGlwdjQYDyABKAhCxQHCSMEBClwKC3N0cmluZy5pcHY0EiJ2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgSVB2NCBhZGRyZXNzGikhcnVsZXMuaXB2NCB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNJcCg0KQphChFzdHJpbmcuaXB2NF9lbXB0eRIxdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQdjQgYWRkcmVzcxoZIXJ1bGVzLmlwdjQgfHwgdGhpcyAhPSAnJ0gAEtYBCgRpcHY2GBAgASgIQsUBwkjBAQpcCgtzdHJpbmcuaXB2NhIidmFsdWUgbXVzdCBiZSBhIHZhbGlkIElQdjYgYWRkcmVzcxopIXJ1bGVzLmlwdjYgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSXAoNikKYQoRc3RyaW5nLmlwdjZfZW1wdHkSMXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBJUHY2IGFkZHJlc3MaGSFydWxlcy5pcHY2IHx8IHRoaXMgIT0gJydIABK/AQoDdXJpGBEgASgIQq8BwkirAQpRCgpzdHJpbmcudXJpEhl2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgVVJJGighcnVsZXMudXJpIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5pc1VyaSgpClYKEHN0cmluZy51cmlfZW1wdHkSKHZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBVUkkaGCFydWxlcy51cmkgfHwgdGhpcyAhPSAnJ0gAEnAKB3VyaV9yZWYYEiABKAhCXcJIWgpYCg5zdHJpbmcudXJpX3JlZhIjdmFsdWUgbXVzdCBiZSBhIHZhbGlkIFVSSSBSZWZlcmVuY2UaISFydWxlcy51cmlfcmVmIHx8IHRoaXMuaXNVcmlSZWYoKUgAEpACCgdhZGRyZXNzGBUgASgIQvwBwkj4AQqBAQoOc3RyaW5nLmFkZHJlc3MSLXZhbHVlIG11c3QgYmUgYSB2YWxpZCBob3N0bmFtZSwgb3IgaXAgYWRkcmVzcxpAIXJ1bGVzLmFkZHJlc3MgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSG9zdG5hbWUoKSB8fCB0aGlzLmlzSXAoKQpyChRzdHJpbmcuYWRkcmVzc19lbXB0eRI8dmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIGhvc3RuYW1lLCBvciBpcCBhZGRyZXNzGhwhcnVsZXMuYWRkcmVzcyB8fCB0aGlzICE9ICcnSAASmAIKBHV1aWQYFiABKAhChwLCSIMCCqUBCgtzdHJpbmcudXVpZBIadmFsdWUgbXVzdCBiZSBhIHZhbGlkIFVVSUQaeiFydWxlcy51dWlkIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5tYXRjaGVzKCdeWzAtOWEtZkEtRl17OH0tWzAtOWEtZkEtRl17NH0tWzAtOWEtZkEtRl17NH0tWzAtOWEtZkEtRl17NH0tWzAtOWEtZkEtRl17MTJ9JCcpClkKEXN0cmluZy51dWlkX2VtcHR5Eil2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgVVVJRBoZIXJ1bGVzLnV1aWQgfHwgdGhpcyAhPSAnJ0gAEvABCgV0dXVpZBghIAEoCELeAcJI2gEKcwoMc3RyaW5nLnR1dWlkEiJ2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgdHJpbW1lZCBVVUlEGj8hcnVsZXMudHV1aWQgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLm1hdGNoZXMoJ15bMC05YS1mQS1GXXszMn0kJykKYwoSc3RyaW5nLnR1dWlkX2VtcHR5EjF2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgdHJpbW1lZCBVVUlEGhohcnVsZXMudHV1aWQgfHwgdGhpcyAhPSAnJ0gAEpYCChFpcF93aXRoX3ByZWZpeGxlbhgaIAEoCEL4AcJI9AEKeAoYc3RyaW5nLmlwX3dpdGhfcHJlZml4bGVuEh92YWx1ZSBtdXN0IGJlIGEgdmFsaWQgSVAgcHJlZml4GjshcnVsZXMuaXBfd2l0aF9wcmVmaXhsZW4gfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSXBQcmVmaXgoKQp4Ch5zdHJpbmcuaXBfd2l0aF9wcmVmaXhsZW5fZW1wdHkSLnZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBJUCBwcmVmaXgaJiFydWxlcy5pcF93aXRoX3ByZWZpeGxlbiB8fCB0aGlzICE9ICcnSAASzwIKE2lwdjRfd2l0aF9wcmVmaXhsZW4YGyABKAhCrwLCSKsCCpMBChpzdHJpbmcuaXB2NF93aXRoX3ByZWZpeGxlbhI1dmFsdWUgbXVzdCBiZSBhIHZhbGlkIElQdjQgYWRkcmVzcyB3aXRoIHByZWZpeCBsZW5ndGgaPiFydWxlcy5pcHY0X3dpdGhfcHJlZml4bGVuIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5pc0lwUHJlZml4KDQpCpIBCiBzdHJpbmcuaXB2NF93aXRoX3ByZWZpeGxlbl9lbXB0eRJEdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQdjQgYWRkcmVzcyB3aXRoIHByZWZpeCBsZW5ndGgaKCFydWxlcy5pcHY0X3dpdGhfcHJlZml4bGVuIHx8IHRoaXMgIT0gJydIABLPAgoTaXB2Nl93aXRoX3ByZWZpeGxlbhgcIAEoCEKvAsJIqwIKkwEKGnN0cmluZy5pcHY2X3dpdGhfcHJlZml4bGVuEjV2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgSVB2NiBhZGRyZXNzIHdpdGggcHJlZml4IGxlbmd0aBo+IXJ1bGVzLmlwdjZfd2l0aF9wcmVmaXhsZW4gfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSXBQcmVmaXgoNikKkgEKIHN0cmluZy5pcHY2X3dpdGhfcHJlZml4bGVuX2VtcHR5EkR2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgSVB2NiBhZGRyZXNzIHdpdGggcHJlZml4IGxlbmd0aBooIXJ1bGVzLmlwdjZfd2l0aF9wcmVmaXhsZW4gfHwgdGhpcyAhPSAnJ0gAEvIBCglpcF9wcmVmaXgYHSABKAhC3AHCSNgBCmwKEHN0cmluZy5pcF9wcmVmaXgSH3ZhbHVlIG11c3QgYmUgYSB2YWxpZCBJUCBwcmVmaXgaNyFydWxlcy5pcF9wcmVmaXggfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSXBQcmVmaXgodHJ1ZSkKaAoWc3RyaW5nLmlwX3ByZWZpeF9lbXB0eRIudmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQIHByZWZpeBoeIXJ1bGVzLmlwX3ByZWZpeCB8fCB0aGlzICE9ICcnSAASgwIKC2lwdjRfcHJlZml4GB4gASgIQusBwkjnAQp1ChJzdHJpbmcuaXB2NF9wcmVmaXgSIXZhbHVlIG11c3QgYmUgYSB2YWxpZCBJUHY0IHByZWZpeBo8IXJ1bGVzLmlwdjRfcHJlZml4IHx8IHRoaXMgPT0gJycgfHwgdGhpcy5pc0lwUHJlZml4KDQsIHRydWUpCm4KGHN0cmluZy5pcHY0X3ByZWZpeF9lbXB0eRIwdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQdjQgcHJlZml4GiAhcnVsZXMuaXB2NF9wcmVmaXggfHwgdGhpcyAhPSAnJ0gAEoMCCgtpcHY2X3ByZWZpeBgfIAEoCELrAcJI5wEKdQoSc3RyaW5nLmlwdjZfcHJlZml4EiF2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgSVB2NiBwcmVmaXgaPCFydWxlcy5pcHY2X3ByZWZpeCB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNJcFByZWZpeCg2LCB0cnVlKQpuChhzdHJpbmcuaXB2Nl9wcmVmaXhfZW1wdHkSMHZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBJUHY2IHByZWZpeBogIXJ1bGVzLmlwdjZfcHJlZml4IHx8IHRoaXMgIT0gJydIABK1AgoNaG9zdF9hbmRfcG9ydBggIAEoCEKbAsJIlwIKmQEKFHN0cmluZy5ob3N0X2FuZF9wb3J0EkF2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgaG9zdCAoaG9zdG5hbWUgb3IgSVAgYWRkcmVzcykgYW5kIHBvcnQgcGFpcho+IXJ1bGVzLmhvc3RfYW5kX3BvcnQgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSG9zdEFuZFBvcnQodHJ1ZSkKeQoac3RyaW5nLmhvc3RfYW5kX3BvcnRfZW1wdHkSN3ZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBob3N0IGFuZCBwb3J0IHBhaXIaIiFydWxlcy5ob3N0X2FuZF9wb3J0IHx8IHRoaXMgIT0gJydIABKoBQoQd2VsbF9rbm93bl9yZWdleBgYIAEoDjIYLmJ1Zi52YWxpZGF0ZS5Lbm93blJlZ2V4QvEEwkjtBArwAQojc3RyaW5nLndlbGxfa25vd25fcmVnZXguaGVhZGVyX25hbWUSJnZhbHVlIG11c3QgYmUgYSB2YWxpZCBIVFRQIGhlYWRlciBuYW1lGqABcnVsZXMud2VsbF9rbm93bl9yZWdleCAhPSAxIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5tYXRjaGVzKCFoYXMocnVsZXMuc3RyaWN0KSB8fCBydWxlcy5zdHJpY3QgPydeOj9bMC05YS16QS1aISMkJSZcJyorLS5eX3x+XHg2MF0rJCcgOideW15cdTAwMDBcdTAwMEFcdTAwMERdKyQnKQqNAQopc3RyaW5nLndlbGxfa25vd25fcmVnZXguaGVhZGVyX25hbWVfZW1wdHkSNXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBIVFRQIGhlYWRlciBuYW1lGilydWxlcy53ZWxsX2tub3duX3JlZ2V4ICE9IDEgfHwgdGhpcyAhPSAnJwrnAQokc3RyaW5nLndlbGxfa25vd25fcmVnZXguaGVhZGVyX3ZhbHVlEid2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgSFRUUCBoZWFkZXIgdmFsdWUalQFydWxlcy53ZWxsX2tub3duX3JlZ2V4ICE9IDIgfHwgdGhpcy5tYXRjaGVzKCFoYXMocnVsZXMuc3RyaWN0KSB8fCBydWxlcy5zdHJpY3QgPydeW15cdTAwMDAtXHUwMDA4XHUwMDBBLVx1MDAxRlx1MDA3Rl0qJCcgOideW15cdTAwMDBcdTAwMEFcdTAwMERdKiQnKUgAEg4KBnN0cmljdBgZIAEoCBIsCgdleGFtcGxlGCIgAygJQhvCSBgKFgoOc3RyaW5nLmV4YW1wbGUaBHRydWUqCQjoBxCAgICAAkIMCgp3ZWxsX2tub3duIuoQCgpCeXRlc1J1bGVzEoABCgVjb25zdBgBIAEoDEJxwkhuCmwKC2J5dGVzLmNvbnN0Gl10aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICd2YWx1ZSBtdXN0IGJlICV4Jy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSeAoDbGVuGA0gASgEQmvCSGgKZgoJYnl0ZXMubGVuGll1aW50KHRoaXMuc2l6ZSgpKSAhPSBydWxlcy5sZW4gPyAndmFsdWUgbGVuZ3RoIG11c3QgYmUgJXMgYnl0ZXMnLmZvcm1hdChbcnVsZXMubGVuXSkgOiAnJxKQAQoHbWluX2xlbhgCIAEoBEJ/wkh8CnoKDWJ5dGVzLm1pbl9sZW4aaXVpbnQodGhpcy5zaXplKCkpIDwgcnVsZXMubWluX2xlbiA/ICd2YWx1ZSBsZW5ndGggbXVzdCBiZSBhdCBsZWFzdCAlcyBieXRlcycuZm9ybWF0KFtydWxlcy5taW5fbGVuXSkgOiAnJxKIAQoHbWF4X2xlbhgDIAEoBEJ3wkh0CnIKDWJ5dGVzLm1heF9sZW4aYXVpbnQodGhpcy5zaXplKCkpID4gcnVsZXMubWF4X2xlbiA/ICd2YWx1ZSBtdXN0IGJlIGF0IG1vc3QgJXMgYnl0ZXMnLmZvcm1hdChbcnVsZXMubWF4X2xlbl0pIDogJycSkAEKB3BhdHRlcm4YBCABKAlCf8JIfAp6Cg1ieXRlcy5wYXR0ZXJuGmkhc3RyaW5nKHRoaXMpLm1hdGNoZXMocnVsZXMucGF0dGVybikgPyAndmFsdWUgbXVzdCBtYXRjaCByZWdleCBwYXR0ZXJuIGAlc2AnLmZvcm1hdChbcnVsZXMucGF0dGVybl0pIDogJycSgQEKBnByZWZpeBgFIAEoDEJxwkhuCmwKDGJ5dGVzLnByZWZpeBpcIXRoaXMuc3RhcnRzV2l0aChydWxlcy5wcmVmaXgpID8gJ3ZhbHVlIGRvZXMgbm90IGhhdmUgcHJlZml4ICV4Jy5mb3JtYXQoW3J1bGVzLnByZWZpeF0pIDogJycSfwoGc3VmZml4GAYgASgMQm/CSGwKagoMYnl0ZXMuc3VmZml4GlohdGhpcy5lbmRzV2l0aChydWxlcy5zdWZmaXgpID8gJ3ZhbHVlIGRvZXMgbm90IGhhdmUgc3VmZml4ICV4Jy5mb3JtYXQoW3J1bGVzLnN1ZmZpeF0pIDogJycSgwEKCGNvbnRhaW5zGAcgASgMQnHCSG4KbAoOYnl0ZXMuY29udGFpbnMaWiF0aGlzLmNvbnRhaW5zKHJ1bGVzLmNvbnRhaW5zKSA/ICd2YWx1ZSBkb2VzIG5vdCBjb250YWluICV4Jy5mb3JtYXQoW3J1bGVzLmNvbnRhaW5zXSkgOiAnJxKnAQoCaW4YCCADKAxCmgHCSJYBCpMBCghieXRlcy5pbhqGAWdldEZpZWxkKHJ1bGVzLCAnaW4nKS5zaXplKCkgPiAwICYmICEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ3ZhbHVlIG11c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEnYKBm5vdF9pbhgJIAMoDEJmwkhjCmEKDGJ5dGVzLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEusBCgJpcBgKIAEoCELcAcJI2AEKdAoIYnl0ZXMuaXASIHZhbHVlIG11c3QgYmUgYSB2YWxpZCBJUCBhZGRyZXNzGkYhcnVsZXMuaXAgfHwgdGhpcy5zaXplKCkgPT0gMCB8fCB0aGlzLnNpemUoKSA9PSA0IHx8IHRoaXMuc2l6ZSgpID09IDE2CmAKDmJ5dGVzLmlwX2VtcHR5Ei92YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgSVAgYWRkcmVzcxodIXJ1bGVzLmlwIHx8IHRoaXMuc2l6ZSgpICE9IDBIABLkAQoEaXB2NBgLIAEoCELTAcJIzwEKZQoKYnl0ZXMuaXB2NBIidmFsdWUgbXVzdCBiZSBhIHZhbGlkIElQdjQgYWRkcmVzcxozIXJ1bGVzLmlwdjQgfHwgdGhpcy5zaXplKCkgPT0gMCB8fCB0aGlzLnNpemUoKSA9PSA0CmYKEGJ5dGVzLmlwdjRfZW1wdHkSMXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBJUHY0IGFkZHJlc3MaHyFydWxlcy5pcHY0IHx8IHRoaXMuc2l6ZSgpICE9IDBIABLlAQoEaXB2NhgMIAEoCELUAcJI0AEKZgoKYnl0ZXMuaXB2NhIidmFsdWUgbXVzdCBiZSBhIHZhbGlkIElQdjYgYWRkcmVzcxo0IXJ1bGVzLmlwdjYgfHwgdGhpcy5zaXplKCkgPT0gMCB8fCB0aGlzLnNpemUoKSA9PSAxNgpmChBieXRlcy5pcHY2X2VtcHR5EjF2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgSVB2NiBhZGRyZXNzGh8hcnVsZXMuaXB2NiB8fCB0aGlzLnNpemUoKSAhPSAwSAASKwoHZXhhbXBsZRgOIAMoDEIawkgXChUKDWJ5dGVzLmV4YW1wbGUaBHRydWUqCQjoBxCAgICAAkIMCgp3ZWxsX2tub3duItQDCglFbnVtUnVsZXMSggEKBWNvbnN0GAEgASgFQnPCSHAKbgoKZW51bS5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEhQKDGRlZmluZWRfb25seRgCIAEoCBJ+CgJpbhgDIAMoBUJywkhvCm0KB2VudW0uaW4aYiEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ3ZhbHVlIG11c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEnUKBm5vdF9pbhgEIAMoBUJlwkhiCmAKC2VudW0ubm90X2luGlF0aGlzIGluIHJ1bGVzLm5vdF9pbiA/ICd2YWx1ZSBtdXN0IG5vdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW3J1bGVzLm5vdF9pbl0pIDogJycSKgoHZXhhbXBsZRgFIAMoBUIZwkgWChQKDGVudW0uZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACIvsDCg1SZXBlYXRlZFJ1bGVzEp4BCgltaW5faXRlbXMYASABKARCigHCSIYBCoMBChJyZXBlYXRlZC5taW5faXRlbXMabXVpbnQodGhpcy5zaXplKCkpIDwgcnVsZXMubWluX2l0ZW1zID8gJ3ZhbHVlIG11c3QgY29udGFpbiBhdCBsZWFzdCAlZCBpdGVtKHMpJy5mb3JtYXQoW3J1bGVzLm1pbl9pdGVtc10pIDogJycSogEKCW1heF9pdGVtcxgCIAEoBEKOAcJIigEKhwEKEnJlcGVhdGVkLm1heF9pdGVtcxpxdWludCh0aGlzLnNpemUoKSkgPiBydWxlcy5tYXhfaXRlbXMgPyAndmFsdWUgbXVzdCBjb250YWluIG5vIG1vcmUgdGhhbiAlcyBpdGVtKHMpJy5mb3JtYXQoW3J1bGVzLm1heF9pdGVtc10pIDogJycScAoGdW5pcXVlGAMgASgIQmDCSF0KWwoPcmVwZWF0ZWQudW5pcXVlEihyZXBlYXRlZCB2YWx1ZSBtdXN0IGNvbnRhaW4gdW5pcXVlIGl0ZW1zGh4hcnVsZXMudW5pcXVlIHx8IHRoaXMudW5pcXVlKCkSJwoFaXRlbXMYBCABKAsyGC5idWYudmFsaWRhdGUuRmllbGRSdWxlcyoJCOgHEICAgIACIooDCghNYXBSdWxlcxKPAQoJbWluX3BhaXJzGAEgASgEQnzCSHkKdwoNbWFwLm1pbl9wYWlycxpmdWludCh0aGlzLnNpemUoKSkgPCBydWxlcy5taW5fcGFpcnMgPyAnbWFwIG11c3QgYmUgYXQgbGVhc3QgJWQgZW50cmllcycuZm9ybWF0KFtydWxlcy5taW5fcGFpcnNdKSA6ICcnEo4BCgltYXhfcGFpcnMYAiABKARCe8JIeAp2Cg1tYXAubWF4X3BhaXJzGmV1aW50KHRoaXMuc2l6ZSgpKSA+IHJ1bGVzLm1heF9wYWlycyA/ICdtYXAgbXVzdCBiZSBhdCBtb3N0ICVkIGVudHJpZXMnLmZvcm1hdChbcnVsZXMubWF4X3BhaXJzXSkgOiAnJxImCgRrZXlzGAQgASgLMhguYnVmLnZhbGlkYXRlLkZpZWxkUnVsZXMSKAoGdmFsdWVzGAUgASgLMhguYnVmLnZhbGlkYXRlLkZpZWxkUnVsZXMqCQjoBxCAgICAAiImCghBbnlSdWxlcxIKCgJpbhgCIAMoCRIOCgZub3RfaW4YAyADKAkimRcKDUR1cmF0aW9uUnVsZXMSoQEKBWNvbnN0GAIgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uQnfCSHQKcgoOZHVyYXRpb24uY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKoAQoCbHQYAyABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25Cf8JIfAp6CgtkdXJhdGlvbi5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABK6AQoDbHRlGAQgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uQo8BwkiLAQqIAQoMZHVyYXRpb24ubHRlGnghaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+IHJ1bGVzLmx0ZT8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABLBBwoCZ3QYBSABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25ClwfCSJMHCn0KC2R1cmF0aW9uLmd0Gm4haGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwq2AQoOZHVyYXRpb24uZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCr4BChhkdXJhdGlvbi5ndF9sdF9leGNsdXNpdmUaoQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrGAQoPZHVyYXRpb24uZ3RfbHRlGrIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrOAQoZZHVyYXRpb24uZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAESjQgKA2d0ZRgGIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkLiB8JI3gcKiwEKDGR1cmF0aW9uLmd0ZRp7IWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPCBydWxlcy5ndGU/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCsUBCg9kdXJhdGlvbi5ndGVfbHQasQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKzQEKGWR1cmF0aW9uLmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtUBChBkdXJhdGlvbi5ndGVfbHRlGsABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnCt0BChpkdXJhdGlvbi5ndGVfbHRlX2V4Y2x1c2l2ZRq+AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARKdAQoCaW4YByADKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CdsJIcwpxCgtkdXJhdGlvbi5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSlAEKBm5vdF9pbhgIIAMoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkJpwkhmCmQKD2R1cmF0aW9uLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEkkKB2V4YW1wbGUYCSADKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CHcJIGgoYChBkdXJhdGlvbi5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiKSGAoOVGltZXN0YW1wUnVsZXMSowEKBWNvbnN0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEJ4wkh1CnMKD3RpbWVzdGFtcC5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEqsBCgJsdBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCgAHCSH0KewoMdGltZXN0YW1wLmx0GmshaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAErwBCgNsdGUYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQpABwkiMAQqJAQoNdGltZXN0YW1wLmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASbAoGbHRfbm93GAcgASgIQlrCSFcKVQoQdGltZXN0YW1wLmx0X25vdxpBKHJ1bGVzLmx0X25vdyAmJiB0aGlzID4gbm93KSA/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBub3cnIDogJydIABLHBwoCZ3QYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQpwHwkiYBwp+Cgx0aW1lc3RhbXAuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrcBCg90aW1lc3RhbXAuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCr8BChl0aW1lc3RhbXAuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxwEKEHRpbWVzdGFtcC5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCs8BChp0aW1lc3RhbXAuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAESkwgKA2d0ZRgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBC5wfCSOMHCowBCg10aW1lc3RhbXAuZ3RlGnshaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8IHJ1bGVzLmd0ZT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZV0pIDogJycKxgEKEHRpbWVzdGFtcC5ndGVfbHQasQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKzgEKGnRpbWVzdGFtcC5ndGVfbHRfZXhjbHVzaXZlGq8BaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrWAQoRdGltZXN0YW1wLmd0ZV9sdGUawAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3RlICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJycK3gEKG3RpbWVzdGFtcC5ndGVfbHRlX2V4Y2x1c2l2ZRq+AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARJvCgZndF9ub3cYCCABKAhCXcJIWgpYChB0aW1lc3RhbXAuZ3Rfbm93GkQocnVsZXMuZ3Rfbm93ICYmIHRoaXMgPCBub3cpID8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG5vdycgOiAnJ0gBErgBCgZ3aXRoaW4YCSABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CjAHCSIgBCoUBChB0aW1lc3RhbXAud2l0aGluGnF0aGlzIDwgbm93LXJ1bGVzLndpdGhpbiB8fCB0aGlzID4gbm93K3J1bGVzLndpdGhpbiA/ICd2YWx1ZSBtdXN0IGJlIHdpdGhpbiAlcyBvZiBub3cnLmZvcm1hdChbcnVsZXMud2l0aGluXSkgOiAnJxJLCgdleGFtcGxlGAogAygLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEIewkgbChkKEXRpbWVzdGFtcC5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiI5CgpWaW9sYXRpb25zEisKCnZpb2xhdGlvbnMYASADKAsyFy5idWYudmFsaWRhdGUuVmlvbGF0aW9uIp8BCglWaW9sYXRpb24SJgoFZmllbGQYBSABKAsyFy5idWYudmFsaWRhdGUuRmllbGRQYXRoEiUKBHJ1bGUYBiABKAsyFy5idWYudmFsaWRhdGUuRmllbGRQYXRoEg8KB3J1bGVfaWQYAiABKAkSDwoHbWVzc2FnZRgDIAEoCRIPCgdmb3Jfa2V5GAQgASgISgQIARACUgpmaWVsZF9wYXRoIj0KCUZpZWxkUGF0aBIwCghlbGVtZW50cxgBIAMoCzIeLmJ1Zi52YWxpZGF0ZS5GaWVsZFBhdGhFbGVtZW50IukCChBGaWVsZFBhdGhFbGVtZW50EhQKDGZpZWxkX251bWJlchgBIAEoBRISCgpmaWVsZF9uYW1lGAIgASgJEj4KCmZpZWxkX3R5cGUYAyABKA4yKi5nb29nbGUucHJvdG9idWYuRmllbGREZXNjcmlwdG9yUHJvdG8uVHlwZRI8CghrZXlfdHlwZRgEIAEoDjIqLmdvb2dsZS5wcm90b2J1Zi5GaWVsZERlc2NyaXB0b3JQcm90by5UeXBlEj4KCnZhbHVlX3R5cGUYBSABKA4yKi5nb29nbGUucHJvdG9idWYuRmllbGREZXNjcmlwdG9yUHJvdG8uVHlwZRIPCgVpbmRleBgGIAEoBEgAEhIKCGJvb2xfa2V5GAcgASgISAASEQoHaW50X2tleRgIIAEoA0gAEhIKCHVpbnRfa2V5GAkgASgESAASFAoKc3RyaW5nX2tleRgKIAEoCUgAQgsKCXN1YnNjcmlwdCqhAQoGSWdub3JlEhYKEklHTk9SRV9VTlNQRUNJRklFRBAAEhgKFElHTk9SRV9JRl9aRVJPX1ZBTFVFEAESEQoNSUdOT1JFX0FMV0FZUxADIgQIAhACKgxJR05PUkVfRU1QVFkqDklHTk9SRV9ERUZBVUxUKhdJR05PUkVfSUZfREVGQVVMVF9WQUxVRSoVSUdOT1JFX0lGX1VOUE9QVUxBVEVEKm4KCktub3duUmVnZXgSGwoXS05PV05fUkVHRVhfVU5TUEVDSUZJRUQQABIgChxLTk9XTl9SRUdFWF9IVFRQX0hFQURFUl9OQU1FEAESIQodS05PV05fUkVHRVhfSFRUUF9IRUFERVJfVkFMVUUQAjpWCgdtZXNzYWdlEh8uZ29vZ2xlLnByb3RvYnVmLk1lc3NhZ2VPcHRpb25zGIcJIAEoCzIaLmJ1Zi52YWxpZGF0ZS5NZXNzYWdlUnVsZXNSB21lc3NhZ2U6TgoFb25lb2YSHS5nb29nbGUucHJvdG9idWYuT25lb2ZPcHRpb25zGIcJIAEoCzIYLmJ1Zi52YWxpZGF0ZS5PbmVvZlJ1bGVzUgVvbmVvZjpOCgVmaWVsZBIdLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE9wdGlvbnMYhwkgASgLMhguYnVmLnZhbGlkYXRlLkZpZWxkUnVsZXNSBWZpZWxkOl0KCnByZWRlZmluZWQSHS5nb29nbGUucHJvdG9idWYuRmllbGRPcHRpb25zGIgJIAEoCzIdLmJ1Zi52YWxpZGF0ZS5QcmVkZWZpbmVkUnVsZXNSCnByZWRlZmluZWRCbgoSYnVpbGQuYnVmLnZhbGlkYXRlQg1WYWxpZGF0ZVByb3RvUAFaR2J1Zi5idWlsZC9nZW4vZ28vYnVmYnVpbGQvcHJvdG92YWxpZGF0ZS9wcm90b2NvbGJ1ZmZlcnMvZ28vYnVmL3ZhbGlkYXRl\", [file_google_protobuf_descriptor, file_google_protobuf_duration, file_google_protobuf_timestamp]);\n\n/**\n * `Rule` represents a validation rule written in the Common Expression\n * Language (CEL) syntax. Each Rule includes a unique identifier, an\n * optional error message, and the CEL expression to evaluate. For more\n * information, [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).\n *\n * ```proto\n * message Foo {\n *   option (buf.validate.message).cel = {\n *     id: \"foo.bar\"\n *     message: \"bar must be greater than 0\"\n *     expression: \"this.bar > 0\"\n *   };\n *   int32 bar = 1;\n * }\n * ```\n *\n * @generated from message buf.validate.Rule\n */\nexport type Rule = Message<\"buf.validate.Rule\"> & {\n  /**\n   * `id` is a string that serves as a machine-readable name for this Rule.\n   * It should be unique within its scope, which could be either a message or a field.\n   *\n   * @generated from field: optional string id = 1;\n   */\n  id: string;\n\n  /**\n   * `message` is an optional field that provides a human-readable error message\n   * for this Rule when the CEL expression evaluates to false. If a\n   * non-empty message is provided, any strings resulting from the CEL\n   * expression evaluation are ignored.\n   *\n   * @generated from field: optional string message = 2;\n   */\n  message: string;\n\n  /**\n   * `expression` is the actual CEL expression that will be evaluated for\n   * validation. This string must resolve to either a boolean or a string\n   * value. If the expression evaluates to false or a non-empty string, the\n   * validation is considered failed, and the message is rejected.\n   *\n   * @generated from field: optional string expression = 3;\n   */\n  expression: string;\n};\n\n/**\n * Describes the message buf.validate.Rule.\n * Use `create(RuleSchema)` to create a new message.\n */\nexport const RuleSchema: GenMessage<Rule> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 0);\n\n/**\n * MessageRules represents validation rules that are applied to the entire message.\n * It includes disabling options and a list of Rule messages representing Common Expression Language (CEL) validation rules.\n *\n * @generated from message buf.validate.MessageRules\n */\nexport type MessageRules = Message<\"buf.validate.MessageRules\"> & {\n  /**\n   * `cel` is a repeated field of type Rule. Each Rule specifies a validation rule to be applied to this message.\n   * These rules are written in Common Expression Language (CEL) syntax. For more information,\n   * [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).\n   *\n   *\n   * ```proto\n   * message MyMessage {\n   *   // The field `foo` must be greater than 42.\n   *   option (buf.validate.message).cel = {\n   *     id: \"my_message.value\",\n   *     message: \"value must be greater than 42\",\n   *     expression: \"this.foo > 42\",\n   *   };\n   *   optional int32 foo = 1;\n   * }\n   * ```\n   *\n   * @generated from field: repeated buf.validate.Rule cel = 3;\n   */\n  cel: Rule[];\n\n  /**\n   * `oneof` is a repeated field of type MessageOneofRule that specifies a list of fields\n   * of which at most one can be present. If `required` is also specified, then exactly one\n   * of the specified fields _must_ be present.\n   *\n   * This will enforce oneof-like constraints with a few features not provided by\n   * actual Protobuf oneof declarations:\n   *   1. Repeated and map fields are allowed in this validation. In a Protobuf oneof,\n   *      only scalar fields are allowed.\n   *   2. Fields with implicit presence are allowed. In a Protobuf oneof, all member\n   *      fields have explicit presence. This means that, for the purpose of determining\n   *      how many fields are set, explicitly setting such a field to its zero value is\n   *      effectively the same as not setting it at all.\n   *   3. This will always generate validation errors for a message unmarshalled from\n   *      serialized data that sets more than one field. With a Protobuf oneof, when\n   *      multiple fields are present in the serialized form, earlier values are usually\n   *      silently ignored when unmarshalling, with only the last field being set when\n   *      unmarshalling completes.\n   *\n   * Note that adding a field to a `oneof` will also set the IGNORE_IF_ZERO_VALUE on the fields. This means\n   * only the field that is set will be validated and the unset fields are not validated according to the field rules.\n   * This behavior can be overridden by setting `ignore` against a field.\n   *\n   * ```proto\n   * message MyMessage {\n   *   // Only one of `field1` or `field2` _can_ be present in this message.\n   *   option (buf.validate.message).oneof = { fields: [\"field1\", \"field2\"] };\n   *   // Exactly one of `field3` or `field4` _must_ be present in this message.\n   *   option (buf.validate.message).oneof = { fields: [\"field3\", \"field4\"], required: true };\n   *   string field1 = 1;\n   *   bytes field2 = 2;\n   *   bool field3 = 3;\n   *   int32 field4 = 4;\n   * }\n   * ```\n   *\n   * @generated from field: repeated buf.validate.MessageOneofRule oneof = 4;\n   */\n  oneof: MessageOneofRule[];\n};\n\n/**\n * Describes the message buf.validate.MessageRules.\n * Use `create(MessageRulesSchema)` to create a new message.\n */\nexport const MessageRulesSchema: GenMessage<MessageRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 1);\n\n/**\n * @generated from message buf.validate.MessageOneofRule\n */\nexport type MessageOneofRule = Message<\"buf.validate.MessageOneofRule\"> & {\n  /**\n   * A list of field names to include in the oneof. All field names must be\n   * defined in the message. At least one field must be specified, and\n   * duplicates are not permitted.\n   *\n   * @generated from field: repeated string fields = 1;\n   */\n  fields: string[];\n\n  /**\n   * If true, one of the fields specified _must_ be set.\n   *\n   * @generated from field: optional bool required = 2;\n   */\n  required: boolean;\n};\n\n/**\n * Describes the message buf.validate.MessageOneofRule.\n * Use `create(MessageOneofRuleSchema)` to create a new message.\n */\nexport const MessageOneofRuleSchema: GenMessage<MessageOneofRule> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 2);\n\n/**\n * The `OneofRules` message type enables you to manage rules for\n * oneof fields in your protobuf messages.\n *\n * @generated from message buf.validate.OneofRules\n */\nexport type OneofRules = Message<\"buf.validate.OneofRules\"> & {\n  /**\n   * If `required` is true, exactly one field of the oneof must be set. A\n   * validation error is returned if no fields in the oneof are set. Further rules\n   * should be placed on the fields themselves to ensure they are valid values,\n   * such as `min_len` or `gt`.\n   *\n   * ```proto\n   * message MyMessage {\n   *   oneof value {\n   *     // Either `a` or `b` must be set. If `a` is set, it must also be\n   *     // non-empty; whereas if `b` is set, it can still be an empty string.\n   *     option (buf.validate.oneof).required = true;\n   *     string a = 1 [(buf.validate.field).string.min_len = 1];\n   *     string b = 2;\n   *   }\n   * }\n   * ```\n   *\n   * @generated from field: optional bool required = 1;\n   */\n  required: boolean;\n};\n\n/**\n * Describes the message buf.validate.OneofRules.\n * Use `create(OneofRulesSchema)` to create a new message.\n */\nexport const OneofRulesSchema: GenMessage<OneofRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 3);\n\n/**\n * FieldRules encapsulates the rules for each type of field. Depending on\n * the field, the correct set should be used to ensure proper validations.\n *\n * @generated from message buf.validate.FieldRules\n */\nexport type FieldRules = Message<\"buf.validate.FieldRules\"> & {\n  /**\n   * `cel` is a repeated field used to represent a textual expression\n   * in the Common Expression Language (CEL) syntax. For more information,\n   * [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).\n   *\n   * ```proto\n   * message MyMessage {\n   *   // The field `value` must be greater than 42.\n   *   optional int32 value = 1 [(buf.validate.field).cel = {\n   *     id: \"my_message.value\",\n   *     message: \"value must be greater than 42\",\n   *     expression: \"this > 42\",\n   *   }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated buf.validate.Rule cel = 23;\n   */\n  cel: Rule[];\n\n  /**\n   * If `required` is true, the field must be set. A validation error is returned\n   * if the field is not set.\n   *\n   * ```proto\n   * syntax=\"proto3\";\n   *\n   * message FieldsWithPresence {\n   *   // Requires any string to be set, including the empty string.\n   *   optional string link = 1 [\n   *     (buf.validate.field).required = true\n   *   ];\n   *   // Requires true or false to be set.\n   *   optional bool disabled = 2 [\n   *     (buf.validate.field).required = true\n   *   ];\n   *   // Requires a message to be set, including the empty message.\n   *   SomeMessage msg = 4 [\n   *     (buf.validate.field).required = true\n   *   ];\n   * }\n   * ```\n   *\n   * All fields in the example above track presence. By default, Protovalidate\n   * ignores rules on those fields if no value is set. `required` ensures that\n   * the fields are set and valid.\n   *\n   * Fields that don't track presence are always validated by Protovalidate,\n   * whether they are set or not. It is not necessary to add `required`. It\n   * can be added to indicate that the field cannot be the zero value.\n   *\n   * ```proto\n   * syntax=\"proto3\";\n   *\n   * message FieldsWithoutPresence {\n   *   // `string.email` always applies, even to an empty string.\n   *   string link = 1 [\n   *     (buf.validate.field).string.email = true\n   *   ];\n   *   // `repeated.min_items` always applies, even to an empty list.\n   *   repeated string labels = 2 [\n   *     (buf.validate.field).repeated.min_items = 1\n   *   ];\n   *   // `required`, for fields that don't track presence, indicates\n   *   // the value of the field can't be the zero value.\n   *   int32 zero_value_not_allowed = 3 [\n   *     (buf.validate.field).required = true\n   *   ];\n   * }\n   * ```\n   *\n   * To learn which fields track presence, see the\n   * [Field Presence cheat sheet](https://protobuf.dev/programming-guides/field_presence/#cheat).\n   *\n   * Note: While field rules can be applied to repeated items, map keys, and map\n   * values, the elements are always considered to be set. Consequently,\n   * specifying `repeated.items.required` is redundant.\n   *\n   * @generated from field: optional bool required = 25;\n   */\n  required: boolean;\n\n  /**\n   * Ignore validation rules on the field if its value matches the specified\n   * criteria. See the `Ignore` enum for details.\n   *\n   * ```proto\n   * message UpdateRequest {\n   *   // The uri rule only applies if the field is not an empty string.\n   *   string url = 1 [\n   *     (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE,\n   *     (buf.validate.field).string.uri = true\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: optional buf.validate.Ignore ignore = 27;\n   */\n  ignore: Ignore;\n\n  /**\n   * @generated from oneof buf.validate.FieldRules.type\n   */\n  type: {\n    /**\n     * Scalar Field Types\n     *\n     * @generated from field: buf.validate.FloatRules float = 1;\n     */\n    value: FloatRules;\n    case: \"float\";\n  } | {\n    /**\n     * @generated from field: buf.validate.DoubleRules double = 2;\n     */\n    value: DoubleRules;\n    case: \"double\";\n  } | {\n    /**\n     * @generated from field: buf.validate.Int32Rules int32 = 3;\n     */\n    value: Int32Rules;\n    case: \"int32\";\n  } | {\n    /**\n     * @generated from field: buf.validate.Int64Rules int64 = 4;\n     */\n    value: Int64Rules;\n    case: \"int64\";\n  } | {\n    /**\n     * @generated from field: buf.validate.UInt32Rules uint32 = 5;\n     */\n    value: UInt32Rules;\n    case: \"uint32\";\n  } | {\n    /**\n     * @generated from field: buf.validate.UInt64Rules uint64 = 6;\n     */\n    value: UInt64Rules;\n    case: \"uint64\";\n  } | {\n    /**\n     * @generated from field: buf.validate.SInt32Rules sint32 = 7;\n     */\n    value: SInt32Rules;\n    case: \"sint32\";\n  } | {\n    /**\n     * @generated from field: buf.validate.SInt64Rules sint64 = 8;\n     */\n    value: SInt64Rules;\n    case: \"sint64\";\n  } | {\n    /**\n     * @generated from field: buf.validate.Fixed32Rules fixed32 = 9;\n     */\n    value: Fixed32Rules;\n    case: \"fixed32\";\n  } | {\n    /**\n     * @generated from field: buf.validate.Fixed64Rules fixed64 = 10;\n     */\n    value: Fixed64Rules;\n    case: \"fixed64\";\n  } | {\n    /**\n     * @generated from field: buf.validate.SFixed32Rules sfixed32 = 11;\n     */\n    value: SFixed32Rules;\n    case: \"sfixed32\";\n  } | {\n    /**\n     * @generated from field: buf.validate.SFixed64Rules sfixed64 = 12;\n     */\n    value: SFixed64Rules;\n    case: \"sfixed64\";\n  } | {\n    /**\n     * @generated from field: buf.validate.BoolRules bool = 13;\n     */\n    value: BoolRules;\n    case: \"bool\";\n  } | {\n    /**\n     * @generated from field: buf.validate.StringRules string = 14;\n     */\n    value: StringRules;\n    case: \"string\";\n  } | {\n    /**\n     * @generated from field: buf.validate.BytesRules bytes = 15;\n     */\n    value: BytesRules;\n    case: \"bytes\";\n  } | {\n    /**\n     * Complex Field Types\n     *\n     * @generated from field: buf.validate.EnumRules enum = 16;\n     */\n    value: EnumRules;\n    case: \"enum\";\n  } | {\n    /**\n     * @generated from field: buf.validate.RepeatedRules repeated = 18;\n     */\n    value: RepeatedRules;\n    case: \"repeated\";\n  } | {\n    /**\n     * @generated from field: buf.validate.MapRules map = 19;\n     */\n    value: MapRules;\n    case: \"map\";\n  } | {\n    /**\n     * Well-Known Field Types\n     *\n     * @generated from field: buf.validate.AnyRules any = 20;\n     */\n    value: AnyRules;\n    case: \"any\";\n  } | {\n    /**\n     * @generated from field: buf.validate.DurationRules duration = 21;\n     */\n    value: DurationRules;\n    case: \"duration\";\n  } | {\n    /**\n     * @generated from field: buf.validate.TimestampRules timestamp = 22;\n     */\n    value: TimestampRules;\n    case: \"timestamp\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message buf.validate.FieldRules.\n * Use `create(FieldRulesSchema)` to create a new message.\n */\nexport const FieldRulesSchema: GenMessage<FieldRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 4);\n\n/**\n * PredefinedRules are custom rules that can be re-used with\n * multiple fields.\n *\n * @generated from message buf.validate.PredefinedRules\n */\nexport type PredefinedRules = Message<\"buf.validate.PredefinedRules\"> & {\n  /**\n   * `cel` is a repeated field used to represent a textual expression\n   * in the Common Expression Language (CEL) syntax. For more information,\n   * [see our documentation](https://buf.build/docs/protovalidate/schemas/predefined-rules/).\n   *\n   * ```proto\n   * message MyMessage {\n   *   // The field `value` must be greater than 42.\n   *   optional int32 value = 1 [(buf.validate.predefined).cel = {\n   *     id: \"my_message.value\",\n   *     message: \"value must be greater than 42\",\n   *     expression: \"this > 42\",\n   *   }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated buf.validate.Rule cel = 1;\n   */\n  cel: Rule[];\n};\n\n/**\n * Describes the message buf.validate.PredefinedRules.\n * Use `create(PredefinedRulesSchema)` to create a new message.\n */\nexport const PredefinedRulesSchema: GenMessage<PredefinedRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 5);\n\n/**\n * FloatRules describes the rules applied to `float` values. These\n * rules may also be applied to the `google.protobuf.FloatValue` Well-Known-Type.\n *\n * @generated from message buf.validate.FloatRules\n */\nexport type FloatRules = Message<\"buf.validate.FloatRules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyFloat {\n   *   // value must equal 42.0\n   *   float value = 1 [(buf.validate.field).float.const = 42.0];\n   * }\n   * ```\n   *\n   * @generated from field: optional float const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.FloatRules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFloat {\n     *   // value must be less than 10.0\n     *   float value = 1 [(buf.validate.field).float.lt = 10.0];\n     * }\n     * ```\n     *\n     * @generated from field: float lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyFloat {\n     *   // value must be less than or equal to 10.0\n     *   float value = 1 [(buf.validate.field).float.lte = 10.0];\n     * }\n     * ```\n     *\n     * @generated from field: float lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.FloatRules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFloat {\n     *   // value must be greater than 5.0 [float.gt]\n     *   float value = 1 [(buf.validate.field).float.gt = 5.0];\n     *\n     *   // value must be greater than 5 and less than 10.0 [float.gt_lt]\n     *   float other_value = 2 [(buf.validate.field).float = { gt: 5.0, lt: 10.0 }];\n     *\n     *   // value must be greater than 10 or less than 5.0 [float.gt_lt_exclusive]\n     *   float another_value = 3 [(buf.validate.field).float = { gt: 10.0, lt: 5.0 }];\n     * }\n     * ```\n     *\n     * @generated from field: float gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFloat {\n     *   // value must be greater than or equal to 5.0 [float.gte]\n     *   float value = 1 [(buf.validate.field).float.gte = 5.0];\n     *\n     *   // value must be greater than or equal to 5.0 and less than 10.0 [float.gte_lt]\n     *   float other_value = 2 [(buf.validate.field).float = { gte: 5.0, lt: 10.0 }];\n     *\n     *   // value must be greater than or equal to 10.0 or less than 5.0 [float.gte_lt_exclusive]\n     *   float another_value = 3 [(buf.validate.field).float = { gte: 10.0, lt: 5.0 }];\n     * }\n     * ```\n     *\n     * @generated from field: float gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message\n   * is generated.\n   *\n   * ```proto\n   * message MyFloat {\n   *   // value must be in list [1.0, 2.0, 3.0]\n   *   float value = 1 [(buf.validate.field).float = { in: [1.0, 2.0, 3.0] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated float in = 6;\n   */\n  in: number[];\n\n  /**\n   * `in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyFloat {\n   *   // value must not be in list [1.0, 2.0, 3.0]\n   *   float value = 1 [(buf.validate.field).float = { not_in: [1.0, 2.0, 3.0] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated float not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `finite` requires the field value to be finite. If the field value is\n   * infinite or NaN, an error message is generated.\n   *\n   * @generated from field: optional bool finite = 8;\n   */\n  finite: boolean;\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyFloat {\n   *   float value = 1 [\n   *     (buf.validate.field).float.example = 1.0,\n   *     (buf.validate.field).float.example = inf\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated float example = 9;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.FloatRules.\n * Use `create(FloatRulesSchema)` to create a new message.\n */\nexport const FloatRulesSchema: GenMessage<FloatRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 6);\n\n/**\n * DoubleRules describes the rules applied to `double` values. These\n * rules may also be applied to the `google.protobuf.DoubleValue` Well-Known-Type.\n *\n * @generated from message buf.validate.DoubleRules\n */\nexport type DoubleRules = Message<\"buf.validate.DoubleRules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyDouble {\n   *   // value must equal 42.0\n   *   double value = 1 [(buf.validate.field).double.const = 42.0];\n   * }\n   * ```\n   *\n   * @generated from field: optional double const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.DoubleRules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyDouble {\n     *   // value must be less than 10.0\n     *   double value = 1 [(buf.validate.field).double.lt = 10.0];\n     * }\n     * ```\n     *\n     * @generated from field: double lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified value\n     * (field <= value). If the field value is greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyDouble {\n     *   // value must be less than or equal to 10.0\n     *   double value = 1 [(buf.validate.field).double.lte = 10.0];\n     * }\n     * ```\n     *\n     * @generated from field: double lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.DoubleRules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or `lte`,\n     * the range is reversed, and the field value must be outside the specified\n     * range. If the field value doesn't meet the required conditions, an error\n     * message is generated.\n     *\n     * ```proto\n     * message MyDouble {\n     *   // value must be greater than 5.0 [double.gt]\n     *   double value = 1 [(buf.validate.field).double.gt = 5.0];\n     *\n     *   // value must be greater than 5 and less than 10.0 [double.gt_lt]\n     *   double other_value = 2 [(buf.validate.field).double = { gt: 5.0, lt: 10.0 }];\n     *\n     *   // value must be greater than 10 or less than 5.0 [double.gt_lt_exclusive]\n     *   double another_value = 3 [(buf.validate.field).double = { gt: 10.0, lt: 5.0 }];\n     * }\n     * ```\n     *\n     * @generated from field: double gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyDouble {\n     *   // value must be greater than or equal to 5.0 [double.gte]\n     *   double value = 1 [(buf.validate.field).double.gte = 5.0];\n     *\n     *   // value must be greater than or equal to 5.0 and less than 10.0 [double.gte_lt]\n     *   double other_value = 2 [(buf.validate.field).double = { gte: 5.0, lt: 10.0 }];\n     *\n     *   // value must be greater than or equal to 10.0 or less than 5.0 [double.gte_lt_exclusive]\n     *   double another_value = 3 [(buf.validate.field).double = { gte: 10.0, lt: 5.0 }];\n     * }\n     * ```\n     *\n     * @generated from field: double gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyDouble {\n   *   // value must be in list [1.0, 2.0, 3.0]\n   *   double value = 1 [(buf.validate.field).double = { in: [1.0, 2.0, 3.0] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated double in = 6;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyDouble {\n   *   // value must not be in list [1.0, 2.0, 3.0]\n   *   double value = 1 [(buf.validate.field).double = { not_in: [1.0, 2.0, 3.0] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated double not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `finite` requires the field value to be finite. If the field value is\n   * infinite or NaN, an error message is generated.\n   *\n   * @generated from field: optional bool finite = 8;\n   */\n  finite: boolean;\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyDouble {\n   *   double value = 1 [\n   *     (buf.validate.field).double.example = 1.0,\n   *     (buf.validate.field).double.example = inf\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated double example = 9;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.DoubleRules.\n * Use `create(DoubleRulesSchema)` to create a new message.\n */\nexport const DoubleRulesSchema: GenMessage<DoubleRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 7);\n\n/**\n * Int32Rules describes the rules applied to `int32` values. These\n * rules may also be applied to the `google.protobuf.Int32Value` Well-Known-Type.\n *\n * @generated from message buf.validate.Int32Rules\n */\nexport type Int32Rules = Message<\"buf.validate.Int32Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyInt32 {\n   *   // value must equal 42\n   *   int32 value = 1 [(buf.validate.field).int32.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional int32 const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.Int32Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field\n     * < value). If the field value is equal to or greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyInt32 {\n     *   // value must be less than 10\n     *   int32 value = 1 [(buf.validate.field).int32.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: int32 lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyInt32 {\n     *   // value must be less than or equal to 10\n     *   int32 value = 1 [(buf.validate.field).int32.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: int32 lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.Int32Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyInt32 {\n     *   // value must be greater than 5 [int32.gt]\n     *   int32 value = 1 [(buf.validate.field).int32.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [int32.gt_lt]\n     *   int32 other_value = 2 [(buf.validate.field).int32 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [int32.gt_lt_exclusive]\n     *   int32 another_value = 3 [(buf.validate.field).int32 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: int32 gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified value\n     * (exclusive). If the value of `gte` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyInt32 {\n     *   // value must be greater than or equal to 5 [int32.gte]\n     *   int32 value = 1 [(buf.validate.field).int32.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [int32.gte_lt]\n     *   int32 other_value = 2 [(buf.validate.field).int32 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [int32.gte_lt_exclusive]\n     *   int32 another_value = 3 [(buf.validate.field).int32 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: int32 gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyInt32 {\n   *   // value must be in list [1, 2, 3]\n   *   int32 value = 1 [(buf.validate.field).int32 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int32 in = 6;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error message\n   * is generated.\n   *\n   * ```proto\n   * message MyInt32 {\n   *   // value must not be in list [1, 2, 3]\n   *   int32 value = 1 [(buf.validate.field).int32 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int32 not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyInt32 {\n   *   int32 value = 1 [\n   *     (buf.validate.field).int32.example = 1,\n   *     (buf.validate.field).int32.example = -10\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int32 example = 8;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.Int32Rules.\n * Use `create(Int32RulesSchema)` to create a new message.\n */\nexport const Int32RulesSchema: GenMessage<Int32Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 8);\n\n/**\n * Int64Rules describes the rules applied to `int64` values. These\n * rules may also be applied to the `google.protobuf.Int64Value` Well-Known-Type.\n *\n * @generated from message buf.validate.Int64Rules\n */\nexport type Int64Rules = Message<\"buf.validate.Int64Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyInt64 {\n   *   // value must equal 42\n   *   int64 value = 1 [(buf.validate.field).int64.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional int64 const = 1;\n   */\n  const: bigint;\n\n  /**\n   * @generated from oneof buf.validate.Int64Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyInt64 {\n     *   // value must be less than 10\n     *   int64 value = 1 [(buf.validate.field).int64.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: int64 lt = 2;\n     */\n    value: bigint;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyInt64 {\n     *   // value must be less than or equal to 10\n     *   int64 value = 1 [(buf.validate.field).int64.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: int64 lte = 3;\n     */\n    value: bigint;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.Int64Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyInt64 {\n     *   // value must be greater than 5 [int64.gt]\n     *   int64 value = 1 [(buf.validate.field).int64.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [int64.gt_lt]\n     *   int64 other_value = 2 [(buf.validate.field).int64 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [int64.gt_lt_exclusive]\n     *   int64 another_value = 3 [(buf.validate.field).int64 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: int64 gt = 4;\n     */\n    value: bigint;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyInt64 {\n     *   // value must be greater than or equal to 5 [int64.gte]\n     *   int64 value = 1 [(buf.validate.field).int64.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [int64.gte_lt]\n     *   int64 other_value = 2 [(buf.validate.field).int64 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [int64.gte_lt_exclusive]\n     *   int64 another_value = 3 [(buf.validate.field).int64 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: int64 gte = 5;\n     */\n    value: bigint;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyInt64 {\n   *   // value must be in list [1, 2, 3]\n   *   int64 value = 1 [(buf.validate.field).int64 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int64 in = 6;\n   */\n  in: bigint[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyInt64 {\n   *   // value must not be in list [1, 2, 3]\n   *   int64 value = 1 [(buf.validate.field).int64 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int64 not_in = 7;\n   */\n  notIn: bigint[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyInt64 {\n   *   int64 value = 1 [\n   *     (buf.validate.field).int64.example = 1,\n   *     (buf.validate.field).int64.example = -10\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int64 example = 9;\n   */\n  example: bigint[];\n};\n\n/**\n * Describes the message buf.validate.Int64Rules.\n * Use `create(Int64RulesSchema)` to create a new message.\n */\nexport const Int64RulesSchema: GenMessage<Int64Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 9);\n\n/**\n * UInt32Rules describes the rules applied to `uint32` values. These\n * rules may also be applied to the `google.protobuf.UInt32Value` Well-Known-Type.\n *\n * @generated from message buf.validate.UInt32Rules\n */\nexport type UInt32Rules = Message<\"buf.validate.UInt32Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyUInt32 {\n   *   // value must equal 42\n   *   uint32 value = 1 [(buf.validate.field).uint32.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint32 const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.UInt32Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyUInt32 {\n     *   // value must be less than 10\n     *   uint32 value = 1 [(buf.validate.field).uint32.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: uint32 lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyUInt32 {\n     *   // value must be less than or equal to 10\n     *   uint32 value = 1 [(buf.validate.field).uint32.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: uint32 lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.UInt32Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyUInt32 {\n     *   // value must be greater than 5 [uint32.gt]\n     *   uint32 value = 1 [(buf.validate.field).uint32.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [uint32.gt_lt]\n     *   uint32 other_value = 2 [(buf.validate.field).uint32 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [uint32.gt_lt_exclusive]\n     *   uint32 another_value = 3 [(buf.validate.field).uint32 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: uint32 gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyUInt32 {\n     *   // value must be greater than or equal to 5 [uint32.gte]\n     *   uint32 value = 1 [(buf.validate.field).uint32.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [uint32.gte_lt]\n     *   uint32 other_value = 2 [(buf.validate.field).uint32 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [uint32.gte_lt_exclusive]\n     *   uint32 another_value = 3 [(buf.validate.field).uint32 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: uint32 gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyUInt32 {\n   *   // value must be in list [1, 2, 3]\n   *   uint32 value = 1 [(buf.validate.field).uint32 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated uint32 in = 6;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyUInt32 {\n   *   // value must not be in list [1, 2, 3]\n   *   uint32 value = 1 [(buf.validate.field).uint32 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated uint32 not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyUInt32 {\n   *   uint32 value = 1 [\n   *     (buf.validate.field).uint32.example = 1,\n   *     (buf.validate.field).uint32.example = 10\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated uint32 example = 8;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.UInt32Rules.\n * Use `create(UInt32RulesSchema)` to create a new message.\n */\nexport const UInt32RulesSchema: GenMessage<UInt32Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 10);\n\n/**\n * UInt64Rules describes the rules applied to `uint64` values. These\n * rules may also be applied to the `google.protobuf.UInt64Value` Well-Known-Type.\n *\n * @generated from message buf.validate.UInt64Rules\n */\nexport type UInt64Rules = Message<\"buf.validate.UInt64Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyUInt64 {\n   *   // value must equal 42\n   *   uint64 value = 1 [(buf.validate.field).uint64.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 const = 1;\n   */\n  const: bigint;\n\n  /**\n   * @generated from oneof buf.validate.UInt64Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyUInt64 {\n     *   // value must be less than 10\n     *   uint64 value = 1 [(buf.validate.field).uint64.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: uint64 lt = 2;\n     */\n    value: bigint;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyUInt64 {\n     *   // value must be less than or equal to 10\n     *   uint64 value = 1 [(buf.validate.field).uint64.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: uint64 lte = 3;\n     */\n    value: bigint;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.UInt64Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyUInt64 {\n     *   // value must be greater than 5 [uint64.gt]\n     *   uint64 value = 1 [(buf.validate.field).uint64.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [uint64.gt_lt]\n     *   uint64 other_value = 2 [(buf.validate.field).uint64 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [uint64.gt_lt_exclusive]\n     *   uint64 another_value = 3 [(buf.validate.field).uint64 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: uint64 gt = 4;\n     */\n    value: bigint;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyUInt64 {\n     *   // value must be greater than or equal to 5 [uint64.gte]\n     *   uint64 value = 1 [(buf.validate.field).uint64.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [uint64.gte_lt]\n     *   uint64 other_value = 2 [(buf.validate.field).uint64 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [uint64.gte_lt_exclusive]\n     *   uint64 another_value = 3 [(buf.validate.field).uint64 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: uint64 gte = 5;\n     */\n    value: bigint;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyUInt64 {\n   *   // value must be in list [1, 2, 3]\n   *   uint64 value = 1 [(buf.validate.field).uint64 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated uint64 in = 6;\n   */\n  in: bigint[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyUInt64 {\n   *   // value must not be in list [1, 2, 3]\n   *   uint64 value = 1 [(buf.validate.field).uint64 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated uint64 not_in = 7;\n   */\n  notIn: bigint[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyUInt64 {\n   *   uint64 value = 1 [\n   *     (buf.validate.field).uint64.example = 1,\n   *     (buf.validate.field).uint64.example = -10\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated uint64 example = 8;\n   */\n  example: bigint[];\n};\n\n/**\n * Describes the message buf.validate.UInt64Rules.\n * Use `create(UInt64RulesSchema)` to create a new message.\n */\nexport const UInt64RulesSchema: GenMessage<UInt64Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 11);\n\n/**\n * SInt32Rules describes the rules applied to `sint32` values.\n *\n * @generated from message buf.validate.SInt32Rules\n */\nexport type SInt32Rules = Message<\"buf.validate.SInt32Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MySInt32 {\n   *   // value must equal 42\n   *   sint32 value = 1 [(buf.validate.field).sint32.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional sint32 const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.SInt32Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field\n     * < value). If the field value is equal to or greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MySInt32 {\n     *   // value must be less than 10\n     *   sint32 value = 1 [(buf.validate.field).sint32.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sint32 lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MySInt32 {\n     *   // value must be less than or equal to 10\n     *   sint32 value = 1 [(buf.validate.field).sint32.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sint32 lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.SInt32Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySInt32 {\n     *   // value must be greater than 5 [sint32.gt]\n     *   sint32 value = 1 [(buf.validate.field).sint32.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [sint32.gt_lt]\n     *   sint32 other_value = 2 [(buf.validate.field).sint32 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [sint32.gt_lt_exclusive]\n     *   sint32 another_value = 3 [(buf.validate.field).sint32 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sint32 gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySInt32 {\n     *  // value must be greater than or equal to 5 [sint32.gte]\n     *  sint32 value = 1 [(buf.validate.field).sint32.gte = 5];\n     *\n     *  // value must be greater than or equal to 5 and less than 10 [sint32.gte_lt]\n     *  sint32 other_value = 2 [(buf.validate.field).sint32 = { gte: 5, lt: 10 }];\n     *\n     *  // value must be greater than or equal to 10 or less than 5 [sint32.gte_lt_exclusive]\n     *  sint32 another_value = 3 [(buf.validate.field).sint32 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sint32 gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MySInt32 {\n   *   // value must be in list [1, 2, 3]\n   *   sint32 value = 1 [(buf.validate.field).sint32 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sint32 in = 6;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MySInt32 {\n   *   // value must not be in list [1, 2, 3]\n   *   sint32 value = 1 [(buf.validate.field).sint32 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sint32 not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MySInt32 {\n   *   sint32 value = 1 [\n   *     (buf.validate.field).sint32.example = 1,\n   *     (buf.validate.field).sint32.example = -10\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sint32 example = 8;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.SInt32Rules.\n * Use `create(SInt32RulesSchema)` to create a new message.\n */\nexport const SInt32RulesSchema: GenMessage<SInt32Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 12);\n\n/**\n * SInt64Rules describes the rules applied to `sint64` values.\n *\n * @generated from message buf.validate.SInt64Rules\n */\nexport type SInt64Rules = Message<\"buf.validate.SInt64Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MySInt64 {\n   *   // value must equal 42\n   *   sint64 value = 1 [(buf.validate.field).sint64.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional sint64 const = 1;\n   */\n  const: bigint;\n\n  /**\n   * @generated from oneof buf.validate.SInt64Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field\n     * < value). If the field value is equal to or greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MySInt64 {\n     *   // value must be less than 10\n     *   sint64 value = 1 [(buf.validate.field).sint64.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sint64 lt = 2;\n     */\n    value: bigint;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MySInt64 {\n     *   // value must be less than or equal to 10\n     *   sint64 value = 1 [(buf.validate.field).sint64.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sint64 lte = 3;\n     */\n    value: bigint;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.SInt64Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySInt64 {\n     *   // value must be greater than 5 [sint64.gt]\n     *   sint64 value = 1 [(buf.validate.field).sint64.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [sint64.gt_lt]\n     *   sint64 other_value = 2 [(buf.validate.field).sint64 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [sint64.gt_lt_exclusive]\n     *   sint64 another_value = 3 [(buf.validate.field).sint64 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sint64 gt = 4;\n     */\n    value: bigint;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySInt64 {\n     *   // value must be greater than or equal to 5 [sint64.gte]\n     *   sint64 value = 1 [(buf.validate.field).sint64.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [sint64.gte_lt]\n     *   sint64 other_value = 2 [(buf.validate.field).sint64 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [sint64.gte_lt_exclusive]\n     *   sint64 another_value = 3 [(buf.validate.field).sint64 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sint64 gte = 5;\n     */\n    value: bigint;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message\n   * is generated.\n   *\n   * ```proto\n   * message MySInt64 {\n   *   // value must be in list [1, 2, 3]\n   *   sint64 value = 1 [(buf.validate.field).sint64 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sint64 in = 6;\n   */\n  in: bigint[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MySInt64 {\n   *   // value must not be in list [1, 2, 3]\n   *   sint64 value = 1 [(buf.validate.field).sint64 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sint64 not_in = 7;\n   */\n  notIn: bigint[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MySInt64 {\n   *   sint64 value = 1 [\n   *     (buf.validate.field).sint64.example = 1,\n   *     (buf.validate.field).sint64.example = -10\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sint64 example = 8;\n   */\n  example: bigint[];\n};\n\n/**\n * Describes the message buf.validate.SInt64Rules.\n * Use `create(SInt64RulesSchema)` to create a new message.\n */\nexport const SInt64RulesSchema: GenMessage<SInt64Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 13);\n\n/**\n * Fixed32Rules describes the rules applied to `fixed32` values.\n *\n * @generated from message buf.validate.Fixed32Rules\n */\nexport type Fixed32Rules = Message<\"buf.validate.Fixed32Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value.\n   * If the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyFixed32 {\n   *   // value must equal 42\n   *   fixed32 value = 1 [(buf.validate.field).fixed32.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional fixed32 const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.Fixed32Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFixed32 {\n     *   // value must be less than 10\n     *   fixed32 value = 1 [(buf.validate.field).fixed32.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: fixed32 lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyFixed32 {\n     *   // value must be less than or equal to 10\n     *   fixed32 value = 1 [(buf.validate.field).fixed32.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: fixed32 lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.Fixed32Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFixed32 {\n     *   // value must be greater than 5 [fixed32.gt]\n     *   fixed32 value = 1 [(buf.validate.field).fixed32.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [fixed32.gt_lt]\n     *   fixed32 other_value = 2 [(buf.validate.field).fixed32 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [fixed32.gt_lt_exclusive]\n     *   fixed32 another_value = 3 [(buf.validate.field).fixed32 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: fixed32 gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFixed32 {\n     *   // value must be greater than or equal to 5 [fixed32.gte]\n     *   fixed32 value = 1 [(buf.validate.field).fixed32.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [fixed32.gte_lt]\n     *   fixed32 other_value = 2 [(buf.validate.field).fixed32 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [fixed32.gte_lt_exclusive]\n     *   fixed32 another_value = 3 [(buf.validate.field).fixed32 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: fixed32 gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message\n   * is generated.\n   *\n   * ```proto\n   * message MyFixed32 {\n   *   // value must be in list [1, 2, 3]\n   *   fixed32 value = 1 [(buf.validate.field).fixed32 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated fixed32 in = 6;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyFixed32 {\n   *   // value must not be in list [1, 2, 3]\n   *   fixed32 value = 1 [(buf.validate.field).fixed32 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated fixed32 not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyFixed32 {\n   *   fixed32 value = 1 [\n   *     (buf.validate.field).fixed32.example = 1,\n   *     (buf.validate.field).fixed32.example = 2\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated fixed32 example = 8;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.Fixed32Rules.\n * Use `create(Fixed32RulesSchema)` to create a new message.\n */\nexport const Fixed32RulesSchema: GenMessage<Fixed32Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 14);\n\n/**\n * Fixed64Rules describes the rules applied to `fixed64` values.\n *\n * @generated from message buf.validate.Fixed64Rules\n */\nexport type Fixed64Rules = Message<\"buf.validate.Fixed64Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyFixed64 {\n   *   // value must equal 42\n   *   fixed64 value = 1 [(buf.validate.field).fixed64.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional fixed64 const = 1;\n   */\n  const: bigint;\n\n  /**\n   * @generated from oneof buf.validate.Fixed64Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFixed64 {\n     *   // value must be less than 10\n     *   fixed64 value = 1 [(buf.validate.field).fixed64.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: fixed64 lt = 2;\n     */\n    value: bigint;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyFixed64 {\n     *   // value must be less than or equal to 10\n     *   fixed64 value = 1 [(buf.validate.field).fixed64.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: fixed64 lte = 3;\n     */\n    value: bigint;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.Fixed64Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFixed64 {\n     *   // value must be greater than 5 [fixed64.gt]\n     *   fixed64 value = 1 [(buf.validate.field).fixed64.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [fixed64.gt_lt]\n     *   fixed64 other_value = 2 [(buf.validate.field).fixed64 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [fixed64.gt_lt_exclusive]\n     *   fixed64 another_value = 3 [(buf.validate.field).fixed64 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: fixed64 gt = 4;\n     */\n    value: bigint;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFixed64 {\n     *   // value must be greater than or equal to 5 [fixed64.gte]\n     *   fixed64 value = 1 [(buf.validate.field).fixed64.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [fixed64.gte_lt]\n     *   fixed64 other_value = 2 [(buf.validate.field).fixed64 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [fixed64.gte_lt_exclusive]\n     *   fixed64 another_value = 3 [(buf.validate.field).fixed64 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: fixed64 gte = 5;\n     */\n    value: bigint;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyFixed64 {\n   *   // value must be in list [1, 2, 3]\n   *   fixed64 value = 1 [(buf.validate.field).fixed64 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated fixed64 in = 6;\n   */\n  in: bigint[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyFixed64 {\n   *   // value must not be in list [1, 2, 3]\n   *   fixed64 value = 1 [(buf.validate.field).fixed64 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated fixed64 not_in = 7;\n   */\n  notIn: bigint[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyFixed64 {\n   *   fixed64 value = 1 [\n   *     (buf.validate.field).fixed64.example = 1,\n   *     (buf.validate.field).fixed64.example = 2\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated fixed64 example = 8;\n   */\n  example: bigint[];\n};\n\n/**\n * Describes the message buf.validate.Fixed64Rules.\n * Use `create(Fixed64RulesSchema)` to create a new message.\n */\nexport const Fixed64RulesSchema: GenMessage<Fixed64Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 15);\n\n/**\n * SFixed32Rules describes the rules applied to `fixed32` values.\n *\n * @generated from message buf.validate.SFixed32Rules\n */\nexport type SFixed32Rules = Message<\"buf.validate.SFixed32Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MySFixed32 {\n   *   // value must equal 42\n   *   sfixed32 value = 1 [(buf.validate.field).sfixed32.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional sfixed32 const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.SFixed32Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySFixed32 {\n     *   // value must be less than 10\n     *   sfixed32 value = 1 [(buf.validate.field).sfixed32.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed32 lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MySFixed32 {\n     *   // value must be less than or equal to 10\n     *   sfixed32 value = 1 [(buf.validate.field).sfixed32.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed32 lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.SFixed32Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySFixed32 {\n     *   // value must be greater than 5 [sfixed32.gt]\n     *   sfixed32 value = 1 [(buf.validate.field).sfixed32.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [sfixed32.gt_lt]\n     *   sfixed32 other_value = 2 [(buf.validate.field).sfixed32 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [sfixed32.gt_lt_exclusive]\n     *   sfixed32 another_value = 3 [(buf.validate.field).sfixed32 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed32 gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySFixed32 {\n     *   // value must be greater than or equal to 5 [sfixed32.gte]\n     *   sfixed32 value = 1 [(buf.validate.field).sfixed32.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [sfixed32.gte_lt]\n     *   sfixed32 other_value = 2 [(buf.validate.field).sfixed32 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [sfixed32.gte_lt_exclusive]\n     *   sfixed32 another_value = 3 [(buf.validate.field).sfixed32 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed32 gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MySFixed32 {\n   *   // value must be in list [1, 2, 3]\n   *   sfixed32 value = 1 [(buf.validate.field).sfixed32 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sfixed32 in = 6;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MySFixed32 {\n   *   // value must not be in list [1, 2, 3]\n   *   sfixed32 value = 1 [(buf.validate.field).sfixed32 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sfixed32 not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MySFixed32 {\n   *   sfixed32 value = 1 [\n   *     (buf.validate.field).sfixed32.example = 1,\n   *     (buf.validate.field).sfixed32.example = 2\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sfixed32 example = 8;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.SFixed32Rules.\n * Use `create(SFixed32RulesSchema)` to create a new message.\n */\nexport const SFixed32RulesSchema: GenMessage<SFixed32Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 16);\n\n/**\n * SFixed64Rules describes the rules applied to `fixed64` values.\n *\n * @generated from message buf.validate.SFixed64Rules\n */\nexport type SFixed64Rules = Message<\"buf.validate.SFixed64Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MySFixed64 {\n   *   // value must equal 42\n   *   sfixed64 value = 1 [(buf.validate.field).sfixed64.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional sfixed64 const = 1;\n   */\n  const: bigint;\n\n  /**\n   * @generated from oneof buf.validate.SFixed64Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySFixed64 {\n     *   // value must be less than 10\n     *   sfixed64 value = 1 [(buf.validate.field).sfixed64.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed64 lt = 2;\n     */\n    value: bigint;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MySFixed64 {\n     *   // value must be less than or equal to 10\n     *   sfixed64 value = 1 [(buf.validate.field).sfixed64.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed64 lte = 3;\n     */\n    value: bigint;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.SFixed64Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySFixed64 {\n     *   // value must be greater than 5 [sfixed64.gt]\n     *   sfixed64 value = 1 [(buf.validate.field).sfixed64.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [sfixed64.gt_lt]\n     *   sfixed64 other_value = 2 [(buf.validate.field).sfixed64 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [sfixed64.gt_lt_exclusive]\n     *   sfixed64 another_value = 3 [(buf.validate.field).sfixed64 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed64 gt = 4;\n     */\n    value: bigint;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySFixed64 {\n     *   // value must be greater than or equal to 5 [sfixed64.gte]\n     *   sfixed64 value = 1 [(buf.validate.field).sfixed64.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [sfixed64.gte_lt]\n     *   sfixed64 other_value = 2 [(buf.validate.field).sfixed64 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [sfixed64.gte_lt_exclusive]\n     *   sfixed64 another_value = 3 [(buf.validate.field).sfixed64 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed64 gte = 5;\n     */\n    value: bigint;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MySFixed64 {\n   *   // value must be in list [1, 2, 3]\n   *   sfixed64 value = 1 [(buf.validate.field).sfixed64 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sfixed64 in = 6;\n   */\n  in: bigint[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MySFixed64 {\n   *   // value must not be in list [1, 2, 3]\n   *   sfixed64 value = 1 [(buf.validate.field).sfixed64 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sfixed64 not_in = 7;\n   */\n  notIn: bigint[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MySFixed64 {\n   *   sfixed64 value = 1 [\n   *     (buf.validate.field).sfixed64.example = 1,\n   *     (buf.validate.field).sfixed64.example = 2\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sfixed64 example = 8;\n   */\n  example: bigint[];\n};\n\n/**\n * Describes the message buf.validate.SFixed64Rules.\n * Use `create(SFixed64RulesSchema)` to create a new message.\n */\nexport const SFixed64RulesSchema: GenMessage<SFixed64Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 17);\n\n/**\n * BoolRules describes the rules applied to `bool` values. These rules\n * may also be applied to the `google.protobuf.BoolValue` Well-Known-Type.\n *\n * @generated from message buf.validate.BoolRules\n */\nexport type BoolRules = Message<\"buf.validate.BoolRules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified boolean value.\n   * If the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyBool {\n   *   // value must equal true\n   *   bool value = 1 [(buf.validate.field).bool.const = true];\n   * }\n   * ```\n   *\n   * @generated from field: optional bool const = 1;\n   */\n  const: boolean;\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyBool {\n   *   bool value = 1 [\n   *     (buf.validate.field).bool.example = 1,\n   *     (buf.validate.field).bool.example = 2\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated bool example = 2;\n   */\n  example: boolean[];\n};\n\n/**\n * Describes the message buf.validate.BoolRules.\n * Use `create(BoolRulesSchema)` to create a new message.\n */\nexport const BoolRulesSchema: GenMessage<BoolRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 18);\n\n/**\n * StringRules describes the rules applied to `string` values These\n * rules may also be applied to the `google.protobuf.StringValue` Well-Known-Type.\n *\n * @generated from message buf.validate.StringRules\n */\nexport type StringRules = Message<\"buf.validate.StringRules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value must equal `hello`\n   *   string value = 1 [(buf.validate.field).string.const = \"hello\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string const = 1;\n   */\n  const: string;\n\n  /**\n   * `len` dictates that the field value must have the specified\n   * number of characters (Unicode code points), which may differ from the number\n   * of bytes in the string. If the field value does not meet the specified\n   * length, an error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value length must be 5 characters\n   *   string value = 1 [(buf.validate.field).string.len = 5];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 len = 19;\n   */\n  len: bigint;\n\n  /**\n   * `min_len` specifies that the field value must have at least the specified\n   * number of characters (Unicode code points), which may differ from the number\n   * of bytes in the string. If the field value contains fewer characters, an error\n   * message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value length must be at least 3 characters\n   *   string value = 1 [(buf.validate.field).string.min_len = 3];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 min_len = 2;\n   */\n  minLen: bigint;\n\n  /**\n   * `max_len` specifies that the field value must have no more than the specified\n   * number of characters (Unicode code points), which may differ from the\n   * number of bytes in the string. If the field value contains more characters,\n   * an error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value length must be at most 10 characters\n   *   string value = 1 [(buf.validate.field).string.max_len = 10];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 max_len = 3;\n   */\n  maxLen: bigint;\n\n  /**\n   * `len_bytes` dictates that the field value must have the specified number of\n   * bytes. If the field value does not match the specified length in bytes,\n   * an error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value length must be 6 bytes\n   *   string value = 1 [(buf.validate.field).string.len_bytes = 6];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 len_bytes = 20;\n   */\n  lenBytes: bigint;\n\n  /**\n   * `min_bytes` specifies that the field value must have at least the specified\n   * number of bytes. If the field value contains fewer bytes, an error message\n   * will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value length must be at least 4 bytes\n   *   string value = 1 [(buf.validate.field).string.min_bytes = 4];\n   * }\n   *\n   * ```\n   *\n   * @generated from field: optional uint64 min_bytes = 4;\n   */\n  minBytes: bigint;\n\n  /**\n   * `max_bytes` specifies that the field value must have no more than the\n   * specified number of bytes. If the field value contains more bytes, an\n   * error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value length must be at most 8 bytes\n   *   string value = 1 [(buf.validate.field).string.max_bytes = 8];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 max_bytes = 5;\n   */\n  maxBytes: bigint;\n\n  /**\n   * `pattern` specifies that the field value must match the specified\n   * regular expression (RE2 syntax), with the expression provided without any\n   * delimiters. If the field value doesn't match the regular expression, an\n   * error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value does not match regex pattern `^[a-zA-Z]//$`\n   *   string value = 1 [(buf.validate.field).string.pattern = \"^[a-zA-Z]//$\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string pattern = 6;\n   */\n  pattern: string;\n\n  /**\n   * `prefix` specifies that the field value must have the\n   * specified substring at the beginning of the string. If the field value\n   * doesn't start with the specified prefix, an error message will be\n   * generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value does not have prefix `pre`\n   *   string value = 1 [(buf.validate.field).string.prefix = \"pre\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string prefix = 7;\n   */\n  prefix: string;\n\n  /**\n   * `suffix` specifies that the field value must have the\n   * specified substring at the end of the string. If the field value doesn't\n   * end with the specified suffix, an error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value does not have suffix `post`\n   *   string value = 1 [(buf.validate.field).string.suffix = \"post\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string suffix = 8;\n   */\n  suffix: string;\n\n  /**\n   * `contains` specifies that the field value must have the\n   * specified substring anywhere in the string. If the field value doesn't\n   * contain the specified substring, an error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value does not contain substring `inside`.\n   *   string value = 1 [(buf.validate.field).string.contains = \"inside\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string contains = 9;\n   */\n  contains: string;\n\n  /**\n   * `not_contains` specifies that the field value must not have the\n   * specified substring anywhere in the string. If the field value contains\n   * the specified substring, an error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value contains substring `inside`.\n   *   string value = 1 [(buf.validate.field).string.not_contains = \"inside\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string not_contains = 23;\n   */\n  notContains: string;\n\n  /**\n   * `in` specifies that the field value must be equal to one of the specified\n   * values. If the field value isn't one of the specified values, an error\n   * message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value must be in list [\"apple\", \"banana\"]\n   *   string value = 1 [(buf.validate.field).string.in = \"apple\", (buf.validate.field).string.in = \"banana\"];\n   * }\n   * ```\n   *\n   * @generated from field: repeated string in = 10;\n   */\n  in: string[];\n\n  /**\n   * `not_in` specifies that the field value cannot be equal to any\n   * of the specified values. If the field value is one of the specified values,\n   * an error message will be generated.\n   * ```proto\n   * message MyString {\n   *   // value must not be in list [\"orange\", \"grape\"]\n   *   string value = 1 [(buf.validate.field).string.not_in = \"orange\", (buf.validate.field).string.not_in = \"grape\"];\n   * }\n   * ```\n   *\n   * @generated from field: repeated string not_in = 11;\n   */\n  notIn: string[];\n\n  /**\n   * `WellKnown` rules provide advanced rules against common string\n   * patterns.\n   *\n   * @generated from oneof buf.validate.StringRules.well_known\n   */\n  wellKnown: {\n    /**\n     * `email` specifies that the field value must be a valid email address, for\n     * example \"foo@example.com\".\n     *\n     * Conforms to the definition for a valid email address from the [HTML standard](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address).\n     * Note that this standard willfully deviates from [RFC 5322](https://datatracker.ietf.org/doc/html/rfc5322),\n     * which allows many unexpected forms of email addresses and will easily match\n     * a typographical error.\n     *\n     * If the field value isn't a valid email address, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid email address\n     *   string value = 1 [(buf.validate.field).string.email = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool email = 12;\n     */\n    value: boolean;\n    case: \"email\";\n  } | {\n    /**\n     * `hostname` specifies that the field value must be a valid hostname, for\n     * example \"foo.example.com\".\n     *\n     * A valid hostname follows the rules below:\n     * - The name consists of one or more labels, separated by a dot (\".\").\n     * - Each label can be 1 to 63 alphanumeric characters.\n     * - A label can contain hyphens (\"-\"), but must not start or end with a hyphen.\n     * - The right-most label must not be digits only.\n     * - The name can have a trailing dot—for example, \"foo.example.com.\".\n     * - The name can be 253 characters at most, excluding the optional trailing dot.\n     *\n     * If the field value isn't a valid hostname, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid hostname\n     *   string value = 1 [(buf.validate.field).string.hostname = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool hostname = 13;\n     */\n    value: boolean;\n    case: \"hostname\";\n  } | {\n    /**\n     * `ip` specifies that the field value must be a valid IP (v4 or v6) address.\n     *\n     * IPv4 addresses are expected in the dotted decimal format—for example, \"192.168.5.21\".\n     * IPv6 addresses are expected in their text representation—for example, \"::1\",\n     * or \"2001:0DB8:ABCD:0012::0\".\n     *\n     * Both formats are well-defined in the internet standard [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986).\n     * Zone identifiers for IPv6 addresses (for example, \"fe80::a%en1\") are supported.\n     *\n     * If the field value isn't a valid IP address, an error message will be\n     * generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IP address\n     *   string value = 1 [(buf.validate.field).string.ip = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ip = 14;\n     */\n    value: boolean;\n    case: \"ip\";\n  } | {\n    /**\n     * `ipv4` specifies that the field value must be a valid IPv4 address—for\n     * example \"192.168.5.21\". If the field value isn't a valid IPv4 address, an\n     * error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IPv4 address\n     *   string value = 1 [(buf.validate.field).string.ipv4 = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv4 = 15;\n     */\n    value: boolean;\n    case: \"ipv4\";\n  } | {\n    /**\n     * `ipv6` specifies that the field value must be a valid IPv6 address—for\n     * example \"::1\", or \"d7a:115c:a1e0:ab12:4843:cd96:626b:430b\". If the field\n     * value is not a valid IPv6 address, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IPv6 address\n     *   string value = 1 [(buf.validate.field).string.ipv6 = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv6 = 16;\n     */\n    value: boolean;\n    case: \"ipv6\";\n  } | {\n    /**\n     * `uri` specifies that the field value must be a valid URI, for example\n     * \"https://example.com/foo/bar?baz=quux#frag\".\n     *\n     * URI is defined in the internet standard [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986).\n     * Zone Identifiers in IPv6 address literals are supported ([RFC 6874](https://datatracker.ietf.org/doc/html/rfc6874)).\n     *\n     * If the field value isn't a valid URI, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid URI\n     *   string value = 1 [(buf.validate.field).string.uri = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool uri = 17;\n     */\n    value: boolean;\n    case: \"uri\";\n  } | {\n    /**\n     * `uri_ref` specifies that the field value must be a valid URI Reference—either\n     * a URI such as \"https://example.com/foo/bar?baz=quux#frag\", or a Relative\n     * Reference such as \"./foo/bar?query\".\n     *\n     * URI, URI Reference, and Relative Reference are defined in the internet\n     * standard [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986). Zone\n     * Identifiers in IPv6 address literals are supported ([RFC 6874](https://datatracker.ietf.org/doc/html/rfc6874)).\n     *\n     * If the field value isn't a valid URI Reference, an error message will be\n     * generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid URI Reference\n     *   string value = 1 [(buf.validate.field).string.uri_ref = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool uri_ref = 18;\n     */\n    value: boolean;\n    case: \"uriRef\";\n  } | {\n    /**\n     * `address` specifies that the field value must be either a valid hostname\n     * (for example, \"example.com\"), or a valid IP (v4 or v6) address (for example,\n     * \"192.168.0.1\", or \"::1\"). If the field value isn't a valid hostname or IP,\n     * an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid hostname, or ip address\n     *   string value = 1 [(buf.validate.field).string.address = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool address = 21;\n     */\n    value: boolean;\n    case: \"address\";\n  } | {\n    /**\n     * `uuid` specifies that the field value must be a valid UUID as defined by\n     * [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.2). If the\n     * field value isn't a valid UUID, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid UUID\n     *   string value = 1 [(buf.validate.field).string.uuid = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool uuid = 22;\n     */\n    value: boolean;\n    case: \"uuid\";\n  } | {\n    /**\n     * `tuuid` (trimmed UUID) specifies that the field value must be a valid UUID as\n     * defined by [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.2) with all dashes\n     * omitted. If the field value isn't a valid UUID without dashes, an error message\n     * will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid trimmed UUID\n     *   string value = 1 [(buf.validate.field).string.tuuid = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool tuuid = 33;\n     */\n    value: boolean;\n    case: \"tuuid\";\n  } | {\n    /**\n     * `ip_with_prefixlen` specifies that the field value must be a valid IP\n     * (v4 or v6) address with prefix length—for example, \"192.168.5.21/16\" or\n     * \"2001:0DB8:ABCD:0012::F1/64\". If the field value isn't a valid IP with\n     * prefix length, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IP with prefix length\n     *    string value = 1 [(buf.validate.field).string.ip_with_prefixlen = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ip_with_prefixlen = 26;\n     */\n    value: boolean;\n    case: \"ipWithPrefixlen\";\n  } | {\n    /**\n     * `ipv4_with_prefixlen` specifies that the field value must be a valid\n     * IPv4 address with prefix length—for example, \"192.168.5.21/16\". If the\n     * field value isn't a valid IPv4 address with prefix length, an error\n     * message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IPv4 address with prefix length\n     *    string value = 1 [(buf.validate.field).string.ipv4_with_prefixlen = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv4_with_prefixlen = 27;\n     */\n    value: boolean;\n    case: \"ipv4WithPrefixlen\";\n  } | {\n    /**\n     * `ipv6_with_prefixlen` specifies that the field value must be a valid\n     * IPv6 address with prefix length—for example, \"2001:0DB8:ABCD:0012::F1/64\".\n     * If the field value is not a valid IPv6 address with prefix length,\n     * an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IPv6 address prefix length\n     *    string value = 1 [(buf.validate.field).string.ipv6_with_prefixlen = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv6_with_prefixlen = 28;\n     */\n    value: boolean;\n    case: \"ipv6WithPrefixlen\";\n  } | {\n    /**\n     * `ip_prefix` specifies that the field value must be a valid IP (v4 or v6)\n     * prefix—for example, \"192.168.0.0/16\" or \"2001:0DB8:ABCD:0012::0/64\".\n     *\n     * The prefix must have all zeros for the unmasked bits. For example,\n     * \"2001:0DB8:ABCD:0012::0/64\" designates the left-most 64 bits for the\n     * prefix, and the remaining 64 bits must be zero.\n     *\n     * If the field value isn't a valid IP prefix, an error message will be\n     * generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IP prefix\n     *    string value = 1 [(buf.validate.field).string.ip_prefix = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ip_prefix = 29;\n     */\n    value: boolean;\n    case: \"ipPrefix\";\n  } | {\n    /**\n     * `ipv4_prefix` specifies that the field value must be a valid IPv4\n     * prefix, for example \"192.168.0.0/16\".\n     *\n     * The prefix must have all zeros for the unmasked bits. For example,\n     * \"192.168.0.0/16\" designates the left-most 16 bits for the prefix,\n     * and the remaining 16 bits must be zero.\n     *\n     * If the field value isn't a valid IPv4 prefix, an error message\n     * will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IPv4 prefix\n     *    string value = 1 [(buf.validate.field).string.ipv4_prefix = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv4_prefix = 30;\n     */\n    value: boolean;\n    case: \"ipv4Prefix\";\n  } | {\n    /**\n     * `ipv6_prefix` specifies that the field value must be a valid IPv6 prefix—for\n     * example, \"2001:0DB8:ABCD:0012::0/64\".\n     *\n     * The prefix must have all zeros for the unmasked bits. For example,\n     * \"2001:0DB8:ABCD:0012::0/64\" designates the left-most 64 bits for the\n     * prefix, and the remaining 64 bits must be zero.\n     *\n     * If the field value is not a valid IPv6 prefix, an error message will be\n     * generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IPv6 prefix\n     *    string value = 1 [(buf.validate.field).string.ipv6_prefix = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv6_prefix = 31;\n     */\n    value: boolean;\n    case: \"ipv6Prefix\";\n  } | {\n    /**\n     * `host_and_port` specifies that the field value must be valid host/port\n     * pair—for example, \"example.com:8080\".\n     *\n     * The host can be one of:\n     * - An IPv4 address in dotted decimal format—for example, \"192.168.5.21\".\n     * - An IPv6 address enclosed in square brackets—for example, \"[2001:0DB8:ABCD:0012::F1]\".\n     * - A hostname—for example, \"example.com\".\n     *\n     * The port is separated by a colon. It must be non-empty, with a decimal number\n     * in the range of 0-65535, inclusive.\n     *\n     * @generated from field: bool host_and_port = 32;\n     */\n    value: boolean;\n    case: \"hostAndPort\";\n  } | {\n    /**\n     * `well_known_regex` specifies a common well-known pattern\n     * defined as a regex. If the field value doesn't match the well-known\n     * regex, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid HTTP header value\n     *   string value = 1 [(buf.validate.field).string.well_known_regex = KNOWN_REGEX_HTTP_HEADER_VALUE];\n     * }\n     * ```\n     *\n     * #### KnownRegex\n     *\n     * `well_known_regex` contains some well-known patterns.\n     *\n     * | Name                          | Number | Description                               |\n     * |-------------------------------|--------|-------------------------------------------|\n     * | KNOWN_REGEX_UNSPECIFIED       | 0      |                                           |\n     * | KNOWN_REGEX_HTTP_HEADER_NAME  | 1      | HTTP header name as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2)  |\n     * | KNOWN_REGEX_HTTP_HEADER_VALUE | 2      | HTTP header value as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4) |\n     *\n     * @generated from field: buf.validate.KnownRegex well_known_regex = 24;\n     */\n    value: KnownRegex;\n    case: \"wellKnownRegex\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * This applies to regexes `HTTP_HEADER_NAME` and `HTTP_HEADER_VALUE` to\n   * enable strict header validation. By default, this is true, and HTTP header\n   * validations are [RFC-compliant](https://datatracker.ietf.org/doc/html/rfc7230#section-3). Setting to false will enable looser\n   * validations that only disallow `\\r\\n\\0` characters, which can be used to\n   * bypass header matching rules.\n   *\n   * ```proto\n   * message MyString {\n   *   // The field `value` must have be a valid HTTP headers, but not enforced with strict rules.\n   *   string value = 1 [(buf.validate.field).string.strict = false];\n   * }\n   * ```\n   *\n   * @generated from field: optional bool strict = 25;\n   */\n  strict: boolean;\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyString {\n   *   string value = 1 [\n   *     (buf.validate.field).string.example = \"hello\",\n   *     (buf.validate.field).string.example = \"world\"\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated string example = 34;\n   */\n  example: string[];\n};\n\n/**\n * Describes the message buf.validate.StringRules.\n * Use `create(StringRulesSchema)` to create a new message.\n */\nexport const StringRulesSchema: GenMessage<StringRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 19);\n\n/**\n * BytesRules describe the rules applied to `bytes` values. These rules\n * may also be applied to the `google.protobuf.BytesValue` Well-Known-Type.\n *\n * @generated from message buf.validate.BytesRules\n */\nexport type BytesRules = Message<\"buf.validate.BytesRules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified bytes\n   * value. If the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value must be \"\\x01\\x02\\x03\\x04\"\n   *   bytes value = 1 [(buf.validate.field).bytes.const = \"\\x01\\x02\\x03\\x04\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional bytes const = 1;\n   */\n  const: Uint8Array;\n\n  /**\n   * `len` requires the field value to have the specified length in bytes.\n   * If the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value length must be 4 bytes.\n   *   optional bytes value = 1 [(buf.validate.field).bytes.len = 4];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 len = 13;\n   */\n  len: bigint;\n\n  /**\n   * `min_len` requires the field value to have at least the specified minimum\n   * length in bytes.\n   * If the field value doesn't meet the requirement, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value length must be at least 2 bytes.\n   *   optional bytes value = 1 [(buf.validate.field).bytes.min_len = 2];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 min_len = 2;\n   */\n  minLen: bigint;\n\n  /**\n   * `max_len` requires the field value to have at most the specified maximum\n   * length in bytes.\n   * If the field value exceeds the requirement, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value must be at most 6 bytes.\n   *   optional bytes value = 1 [(buf.validate.field).bytes.max_len = 6];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 max_len = 3;\n   */\n  maxLen: bigint;\n\n  /**\n   * `pattern` requires the field value to match the specified regular\n   * expression ([RE2 syntax](https://github.com/google/re2/wiki/Syntax)).\n   * The value of the field must be valid UTF-8 or validation will fail with a\n   * runtime error.\n   * If the field value doesn't match the pattern, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value must match regex pattern \"^[a-zA-Z0-9]+$\".\n   *   optional bytes value = 1 [(buf.validate.field).bytes.pattern = \"^[a-zA-Z0-9]+$\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string pattern = 4;\n   */\n  pattern: string;\n\n  /**\n   * `prefix` requires the field value to have the specified bytes at the\n   * beginning of the string.\n   * If the field value doesn't meet the requirement, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value does not have prefix \\x01\\x02\n   *   optional bytes value = 1 [(buf.validate.field).bytes.prefix = \"\\x01\\x02\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional bytes prefix = 5;\n   */\n  prefix: Uint8Array;\n\n  /**\n   * `suffix` requires the field value to have the specified bytes at the end\n   * of the string.\n   * If the field value doesn't meet the requirement, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value does not have suffix \\x03\\x04\n   *   optional bytes value = 1 [(buf.validate.field).bytes.suffix = \"\\x03\\x04\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional bytes suffix = 6;\n   */\n  suffix: Uint8Array;\n\n  /**\n   * `contains` requires the field value to have the specified bytes anywhere in\n   * the string.\n   * If the field value doesn't meet the requirement, an error message is generated.\n   *\n   * ```protobuf\n   * message MyBytes {\n   *   // value does not contain \\x02\\x03\n   *   optional bytes value = 1 [(buf.validate.field).bytes.contains = \"\\x02\\x03\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional bytes contains = 7;\n   */\n  contains: Uint8Array;\n\n  /**\n   * `in` requires the field value to be equal to one of the specified\n   * values. If the field value doesn't match any of the specified values, an\n   * error message is generated.\n   *\n   * ```protobuf\n   * message MyBytes {\n   *   // value must in [\"\\x01\\x02\", \"\\x02\\x03\", \"\\x03\\x04\"]\n   *   optional bytes value = 1 [(buf.validate.field).bytes.in = {\"\\x01\\x02\", \"\\x02\\x03\", \"\\x03\\x04\"}];\n   * }\n   * ```\n   *\n   * @generated from field: repeated bytes in = 8;\n   */\n  in: Uint8Array[];\n\n  /**\n   * `not_in` requires the field value to be not equal to any of the specified\n   * values.\n   * If the field value matches any of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value must not in [\"\\x01\\x02\", \"\\x02\\x03\", \"\\x03\\x04\"]\n   *   optional bytes value = 1 [(buf.validate.field).bytes.not_in = {\"\\x01\\x02\", \"\\x02\\x03\", \"\\x03\\x04\"}];\n   * }\n   * ```\n   *\n   * @generated from field: repeated bytes not_in = 9;\n   */\n  notIn: Uint8Array[];\n\n  /**\n   * WellKnown rules provide advanced rules against common byte\n   * patterns\n   *\n   * @generated from oneof buf.validate.BytesRules.well_known\n   */\n  wellKnown: {\n    /**\n     * `ip` ensures that the field `value` is a valid IP address (v4 or v6) in byte format.\n     * If the field value doesn't meet this rule, an error message is generated.\n     *\n     * ```proto\n     * message MyBytes {\n     *   // value must be a valid IP address\n     *   optional bytes value = 1 [(buf.validate.field).bytes.ip = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ip = 10;\n     */\n    value: boolean;\n    case: \"ip\";\n  } | {\n    /**\n     * `ipv4` ensures that the field `value` is a valid IPv4 address in byte format.\n     * If the field value doesn't meet this rule, an error message is generated.\n     *\n     * ```proto\n     * message MyBytes {\n     *   // value must be a valid IPv4 address\n     *   optional bytes value = 1 [(buf.validate.field).bytes.ipv4 = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv4 = 11;\n     */\n    value: boolean;\n    case: \"ipv4\";\n  } | {\n    /**\n     * `ipv6` ensures that the field `value` is a valid IPv6 address in byte format.\n     * If the field value doesn't meet this rule, an error message is generated.\n     * ```proto\n     * message MyBytes {\n     *   // value must be a valid IPv6 address\n     *   optional bytes value = 1 [(buf.validate.field).bytes.ipv6 = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv6 = 12;\n     */\n    value: boolean;\n    case: \"ipv6\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyBytes {\n   *   bytes value = 1 [\n   *     (buf.validate.field).bytes.example = \"\\x01\\x02\",\n   *     (buf.validate.field).bytes.example = \"\\x02\\x03\"\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated bytes example = 14;\n   */\n  example: Uint8Array[];\n};\n\n/**\n * Describes the message buf.validate.BytesRules.\n * Use `create(BytesRulesSchema)` to create a new message.\n */\nexport const BytesRulesSchema: GenMessage<BytesRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 20);\n\n/**\n * EnumRules describe the rules applied to `enum` values.\n *\n * @generated from message buf.validate.EnumRules\n */\nexport type EnumRules = Message<\"buf.validate.EnumRules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified enum value.\n   * If the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * enum MyEnum {\n   *   MY_ENUM_UNSPECIFIED = 0;\n   *   MY_ENUM_VALUE1 = 1;\n   *   MY_ENUM_VALUE2 = 2;\n   * }\n   *\n   * message MyMessage {\n   *   // The field `value` must be exactly MY_ENUM_VALUE1.\n   *   MyEnum value = 1 [(buf.validate.field).enum.const = 1];\n   * }\n   * ```\n   *\n   * @generated from field: optional int32 const = 1;\n   */\n  const: number;\n\n  /**\n   * `defined_only` requires the field value to be one of the defined values for\n   * this enum, failing on any undefined value.\n   *\n   * ```proto\n   * enum MyEnum {\n   *   MY_ENUM_UNSPECIFIED = 0;\n   *   MY_ENUM_VALUE1 = 1;\n   *   MY_ENUM_VALUE2 = 2;\n   * }\n   *\n   * message MyMessage {\n   *   // The field `value` must be a defined value of MyEnum.\n   *   MyEnum value = 1 [(buf.validate.field).enum.defined_only = true];\n   * }\n   * ```\n   *\n   * @generated from field: optional bool defined_only = 2;\n   */\n  definedOnly: boolean;\n\n  /**\n   * `in` requires the field value to be equal to one of the\n   * specified enum values. If the field value doesn't match any of the\n   * specified values, an error message is generated.\n   *\n   * ```proto\n   * enum MyEnum {\n   *   MY_ENUM_UNSPECIFIED = 0;\n   *   MY_ENUM_VALUE1 = 1;\n   *   MY_ENUM_VALUE2 = 2;\n   * }\n   *\n   * message MyMessage {\n   *   // The field `value` must be equal to one of the specified values.\n   *   MyEnum value = 1 [(buf.validate.field).enum = { in: [1, 2]}];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int32 in = 3;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to be not equal to any of the\n   * specified enum values. If the field value matches one of the specified\n   * values, an error message is generated.\n   *\n   * ```proto\n   * enum MyEnum {\n   *   MY_ENUM_UNSPECIFIED = 0;\n   *   MY_ENUM_VALUE1 = 1;\n   *   MY_ENUM_VALUE2 = 2;\n   * }\n   *\n   * message MyMessage {\n   *   // The field `value` must not be equal to any of the specified values.\n   *   MyEnum value = 1 [(buf.validate.field).enum = { not_in: [1, 2]}];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int32 not_in = 4;\n   */\n  notIn: number[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * enum MyEnum {\n   *   MY_ENUM_UNSPECIFIED = 0;\n   *   MY_ENUM_VALUE1 = 1;\n   *   MY_ENUM_VALUE2 = 2;\n   * }\n   *\n   * message MyMessage {\n   *     (buf.validate.field).enum.example = 1,\n   *     (buf.validate.field).enum.example = 2\n   * }\n   * ```\n   *\n   * @generated from field: repeated int32 example = 5;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.EnumRules.\n * Use `create(EnumRulesSchema)` to create a new message.\n */\nexport const EnumRulesSchema: GenMessage<EnumRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 21);\n\n/**\n * RepeatedRules describe the rules applied to `repeated` values.\n *\n * @generated from message buf.validate.RepeatedRules\n */\nexport type RepeatedRules = Message<\"buf.validate.RepeatedRules\"> & {\n  /**\n   * `min_items` requires that this field must contain at least the specified\n   * minimum number of items.\n   *\n   * Note that `min_items = 1` is equivalent to setting a field as `required`.\n   *\n   * ```proto\n   * message MyRepeated {\n   *   // value must contain at least  2 items\n   *   repeated string value = 1 [(buf.validate.field).repeated.min_items = 2];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 min_items = 1;\n   */\n  minItems: bigint;\n\n  /**\n   * `max_items` denotes that this field must not exceed a\n   * certain number of items as the upper limit. If the field contains more\n   * items than specified, an error message will be generated, requiring the\n   * field to maintain no more than the specified number of items.\n   *\n   * ```proto\n   * message MyRepeated {\n   *   // value must contain no more than 3 item(s)\n   *   repeated string value = 1 [(buf.validate.field).repeated.max_items = 3];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 max_items = 2;\n   */\n  maxItems: bigint;\n\n  /**\n   * `unique` indicates that all elements in this field must\n   * be unique. This rule is strictly applicable to scalar and enum\n   * types, with message types not being supported.\n   *\n   * ```proto\n   * message MyRepeated {\n   *   // repeated value must contain unique items\n   *   repeated string value = 1 [(buf.validate.field).repeated.unique = true];\n   * }\n   * ```\n   *\n   * @generated from field: optional bool unique = 3;\n   */\n  unique: boolean;\n\n  /**\n   * `items` details the rules to be applied to each item\n   * in the field. Even for repeated message fields, validation is executed\n   * against each item unless `ignore` is specified.\n   *\n   * ```proto\n   * message MyRepeated {\n   *   // The items in the field `value` must follow the specified rules.\n   *   repeated string value = 1 [(buf.validate.field).repeated.items = {\n   *     string: {\n   *       min_len: 3\n   *       max_len: 10\n   *     }\n   *   }];\n   * }\n   * ```\n   *\n   * Note that the `required` rule does not apply. Repeated items\n   * cannot be unset.\n   *\n   * @generated from field: optional buf.validate.FieldRules items = 4;\n   */\n  items?: FieldRules;\n};\n\n/**\n * Describes the message buf.validate.RepeatedRules.\n * Use `create(RepeatedRulesSchema)` to create a new message.\n */\nexport const RepeatedRulesSchema: GenMessage<RepeatedRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 22);\n\n/**\n * MapRules describe the rules applied to `map` values.\n *\n * @generated from message buf.validate.MapRules\n */\nexport type MapRules = Message<\"buf.validate.MapRules\"> & {\n  /**\n   * Specifies the minimum number of key-value pairs allowed. If the field has\n   * fewer key-value pairs than specified, an error message is generated.\n   *\n   * ```proto\n   * message MyMap {\n   *   // The field `value` must have at least 2 key-value pairs.\n   *   map<string, string> value = 1 [(buf.validate.field).map.min_pairs = 2];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 min_pairs = 1;\n   */\n  minPairs: bigint;\n\n  /**\n   * Specifies the maximum number of key-value pairs allowed. If the field has\n   * more key-value pairs than specified, an error message is generated.\n   *\n   * ```proto\n   * message MyMap {\n   *   // The field `value` must have at most 3 key-value pairs.\n   *   map<string, string> value = 1 [(buf.validate.field).map.max_pairs = 3];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 max_pairs = 2;\n   */\n  maxPairs: bigint;\n\n  /**\n   * Specifies the rules to be applied to each key in the field.\n   *\n   * ```proto\n   * message MyMap {\n   *   // The keys in the field `value` must follow the specified rules.\n   *   map<string, string> value = 1 [(buf.validate.field).map.keys = {\n   *     string: {\n   *       min_len: 3\n   *       max_len: 10\n   *     }\n   *   }];\n   * }\n   * ```\n   *\n   * Note that the `required` rule does not apply. Map keys cannot be unset.\n   *\n   * @generated from field: optional buf.validate.FieldRules keys = 4;\n   */\n  keys?: FieldRules;\n\n  /**\n   * Specifies the rules to be applied to the value of each key in the\n   * field. Message values will still have their validations evaluated unless\n   * `ignore` is specified.\n   *\n   * ```proto\n   * message MyMap {\n   *   // The values in the field `value` must follow the specified rules.\n   *   map<string, string> value = 1 [(buf.validate.field).map.values = {\n   *     string: {\n   *       min_len: 5\n   *       max_len: 20\n   *     }\n   *   }];\n   * }\n   * ```\n   * Note that the `required` rule does not apply. Map values cannot be unset.\n   *\n   * @generated from field: optional buf.validate.FieldRules values = 5;\n   */\n  values?: FieldRules;\n};\n\n/**\n * Describes the message buf.validate.MapRules.\n * Use `create(MapRulesSchema)` to create a new message.\n */\nexport const MapRulesSchema: GenMessage<MapRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 23);\n\n/**\n * AnyRules describe rules applied exclusively to the `google.protobuf.Any` well-known type.\n *\n * @generated from message buf.validate.AnyRules\n */\nexport type AnyRules = Message<\"buf.validate.AnyRules\"> & {\n  /**\n   * `in` requires the field's `type_url` to be equal to one of the\n   * specified values. If it doesn't match any of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyAny {\n   *   //  The `value` field must have a `type_url` equal to one of the specified values.\n   *   google.protobuf.Any value = 1 [(buf.validate.field).any = {\n   *       in: [\"type.googleapis.com/MyType1\", \"type.googleapis.com/MyType2\"]\n   *   }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated string in = 2;\n   */\n  in: string[];\n\n  /**\n   * requires the field's type_url to be not equal to any of the specified values. If it matches any of the specified values, an error message is generated.\n   *\n   * ```proto\n   * message MyAny {\n   *   //  The `value` field must not have a `type_url` equal to any of the specified values.\n   *   google.protobuf.Any value = 1 [(buf.validate.field).any = {\n   *       not_in: [\"type.googleapis.com/ForbiddenType1\", \"type.googleapis.com/ForbiddenType2\"]\n   *   }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated string not_in = 3;\n   */\n  notIn: string[];\n};\n\n/**\n * Describes the message buf.validate.AnyRules.\n * Use `create(AnyRulesSchema)` to create a new message.\n */\nexport const AnyRulesSchema: GenMessage<AnyRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 24);\n\n/**\n * DurationRules describe the rules applied exclusively to the `google.protobuf.Duration` well-known type.\n *\n * @generated from message buf.validate.DurationRules\n */\nexport type DurationRules = Message<\"buf.validate.DurationRules\"> & {\n  /**\n   * `const` dictates that the field must match the specified value of the `google.protobuf.Duration` type exactly.\n   * If the field's value deviates from the specified value, an error message\n   * will be generated.\n   *\n   * ```proto\n   * message MyDuration {\n   *   // value must equal 5s\n   *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.const = \"5s\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional google.protobuf.Duration const = 2;\n   */\n  const?: Duration;\n\n  /**\n   * @generated from oneof buf.validate.DurationRules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` stipulates that the field must be less than the specified value of the `google.protobuf.Duration` type,\n     * exclusive. If the field's value is greater than or equal to the specified\n     * value, an error message will be generated.\n     *\n     * ```proto\n     * message MyDuration {\n     *   // value must be less than 5s\n     *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.lt = \"5s\"];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Duration lt = 3;\n     */\n    value: Duration;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` indicates that the field must be less than or equal to the specified\n     * value of the `google.protobuf.Duration` type, inclusive. If the field's value is greater than the specified value,\n     * an error message will be generated.\n     *\n     * ```proto\n     * message MyDuration {\n     *   // value must be less than or equal to 10s\n     *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.lte = \"10s\"];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Duration lte = 4;\n     */\n    value: Duration;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.DurationRules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the duration field value to be greater than the specified\n     * value (exclusive). If the value of `gt` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyDuration {\n     *   // duration must be greater than 5s [duration.gt]\n     *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.gt = { seconds: 5 }];\n     *\n     *   // duration must be greater than 5s and less than 10s [duration.gt_lt]\n     *   google.protobuf.Duration another_value = 2 [(buf.validate.field).duration = { gt: { seconds: 5 }, lt: { seconds: 10 } }];\n     *\n     *   // duration must be greater than 10s or less than 5s [duration.gt_lt_exclusive]\n     *   google.protobuf.Duration other_value = 3 [(buf.validate.field).duration = { gt: { seconds: 10 }, lt: { seconds: 5 } }];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Duration gt = 5;\n     */\n    value: Duration;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the duration field value to be greater than or equal to the\n     * specified value (exclusive). If the value of `gte` is larger than a\n     * specified `lt` or `lte`, the range is reversed, and the field value must\n     * be outside the specified range. If the field value doesn't meet the\n     * required conditions, an error message is generated.\n     *\n     * ```proto\n     * message MyDuration {\n     *  // duration must be greater than or equal to 5s [duration.gte]\n     *  google.protobuf.Duration value = 1 [(buf.validate.field).duration.gte = { seconds: 5 }];\n     *\n     *  // duration must be greater than or equal to 5s and less than 10s [duration.gte_lt]\n     *  google.protobuf.Duration another_value = 2 [(buf.validate.field).duration = { gte: { seconds: 5 }, lt: { seconds: 10 } }];\n     *\n     *  // duration must be greater than or equal to 10s or less than 5s [duration.gte_lt_exclusive]\n     *  google.protobuf.Duration other_value = 3 [(buf.validate.field).duration = { gte: { seconds: 10 }, lt: { seconds: 5 } }];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Duration gte = 6;\n     */\n    value: Duration;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` asserts that the field must be equal to one of the specified values of the `google.protobuf.Duration` type.\n   * If the field's value doesn't correspond to any of the specified values,\n   * an error message will be generated.\n   *\n   * ```proto\n   * message MyDuration {\n   *   // value must be in list [1s, 2s, 3s]\n   *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.in = [\"1s\", \"2s\", \"3s\"]];\n   * }\n   * ```\n   *\n   * @generated from field: repeated google.protobuf.Duration in = 7;\n   */\n  in: Duration[];\n\n  /**\n   * `not_in` denotes that the field must not be equal to\n   * any of the specified values of the `google.protobuf.Duration` type.\n   * If the field's value matches any of these values, an error message will be\n   * generated.\n   *\n   * ```proto\n   * message MyDuration {\n   *   // value must not be in list [1s, 2s, 3s]\n   *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.not_in = [\"1s\", \"2s\", \"3s\"]];\n   * }\n   * ```\n   *\n   * @generated from field: repeated google.protobuf.Duration not_in = 8;\n   */\n  notIn: Duration[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyDuration {\n   *   google.protobuf.Duration value = 1 [\n   *     (buf.validate.field).duration.example = { seconds: 1 },\n   *     (buf.validate.field).duration.example = { seconds: 2 },\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated google.protobuf.Duration example = 9;\n   */\n  example: Duration[];\n};\n\n/**\n * Describes the message buf.validate.DurationRules.\n * Use `create(DurationRulesSchema)` to create a new message.\n */\nexport const DurationRulesSchema: GenMessage<DurationRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 25);\n\n/**\n * TimestampRules describe the rules applied exclusively to the `google.protobuf.Timestamp` well-known type.\n *\n * @generated from message buf.validate.TimestampRules\n */\nexport type TimestampRules = Message<\"buf.validate.TimestampRules\"> & {\n  /**\n   * `const` dictates that this field, of the `google.protobuf.Timestamp` type, must exactly match the specified value. If the field value doesn't correspond to the specified timestamp, an error message will be generated.\n   *\n   * ```proto\n   * message MyTimestamp {\n   *   // value must equal 2023-05-03T10:00:00Z\n   *   google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.const = {seconds: 1727998800}];\n   * }\n   * ```\n   *\n   * @generated from field: optional google.protobuf.Timestamp const = 2;\n   */\n  const?: Timestamp;\n\n  /**\n   * @generated from oneof buf.validate.TimestampRules.less_than\n   */\n  lessThan: {\n    /**\n     * requires the duration field value to be less than the specified value (field < value). If the field value doesn't meet the required conditions, an error message is generated.\n     *\n     * ```proto\n     * message MyDuration {\n     *   // duration must be less than 'P3D' [duration.lt]\n     *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.lt = { seconds: 259200 }];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Timestamp lt = 3;\n     */\n    value: Timestamp;\n    case: \"lt\";\n  } | {\n    /**\n     * requires the timestamp field value to be less than or equal to the specified value (field <= value). If the field value doesn't meet the required conditions, an error message is generated.\n     *\n     * ```proto\n     * message MyTimestamp {\n     *   // timestamp must be less than or equal to '2023-05-14T00:00:00Z' [timestamp.lte]\n     *   google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.lte = { seconds: 1678867200 }];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Timestamp lte = 4;\n     */\n    value: Timestamp;\n    case: \"lte\";\n  } | {\n    /**\n     * `lt_now` specifies that this field, of the `google.protobuf.Timestamp` type, must be less than the current time. `lt_now` can only be used with the `within` rule.\n     *\n     * ```proto\n     * message MyTimestamp {\n     *  // value must be less than now\n     *   google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.lt_now = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool lt_now = 7;\n     */\n    value: boolean;\n    case: \"ltNow\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.TimestampRules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the timestamp field value to be greater than the specified\n     * value (exclusive). If the value of `gt` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyTimestamp {\n     *   // timestamp must be greater than '2023-01-01T00:00:00Z' [timestamp.gt]\n     *   google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.gt = { seconds: 1672444800 }];\n     *\n     *   // timestamp must be greater than '2023-01-01T00:00:00Z' and less than '2023-01-02T00:00:00Z' [timestamp.gt_lt]\n     *   google.protobuf.Timestamp another_value = 2 [(buf.validate.field).timestamp = { gt: { seconds: 1672444800 }, lt: { seconds: 1672531200 } }];\n     *\n     *   // timestamp must be greater than '2023-01-02T00:00:00Z' or less than '2023-01-01T00:00:00Z' [timestamp.gt_lt_exclusive]\n     *   google.protobuf.Timestamp other_value = 3 [(buf.validate.field).timestamp = { gt: { seconds: 1672531200 }, lt: { seconds: 1672444800 } }];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Timestamp gt = 5;\n     */\n    value: Timestamp;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the timestamp field value to be greater than or equal to the\n     * specified value (exclusive). If the value of `gte` is larger than a\n     * specified `lt` or `lte`, the range is reversed, and the field value\n     * must be outside the specified range. If the field value doesn't meet\n     * the required conditions, an error message is generated.\n     *\n     * ```proto\n     * message MyTimestamp {\n     *   // timestamp must be greater than or equal to '2023-01-01T00:00:00Z' [timestamp.gte]\n     *   google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.gte = { seconds: 1672444800 }];\n     *\n     *   // timestamp must be greater than or equal to '2023-01-01T00:00:00Z' and less than '2023-01-02T00:00:00Z' [timestamp.gte_lt]\n     *   google.protobuf.Timestamp another_value = 2 [(buf.validate.field).timestamp = { gte: { seconds: 1672444800 }, lt: { seconds: 1672531200 } }];\n     *\n     *   // timestamp must be greater than or equal to '2023-01-02T00:00:00Z' or less than '2023-01-01T00:00:00Z' [timestamp.gte_lt_exclusive]\n     *   google.protobuf.Timestamp other_value = 3 [(buf.validate.field).timestamp = { gte: { seconds: 1672531200 }, lt: { seconds: 1672444800 } }];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Timestamp gte = 6;\n     */\n    value: Timestamp;\n    case: \"gte\";\n  } | {\n    /**\n     * `gt_now` specifies that this field, of the `google.protobuf.Timestamp` type, must be greater than the current time. `gt_now` can only be used with the `within` rule.\n     *\n     * ```proto\n     * message MyTimestamp {\n     *   // value must be greater than now\n     *   google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.gt_now = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool gt_now = 8;\n     */\n    value: boolean;\n    case: \"gtNow\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `within` specifies that this field, of the `google.protobuf.Timestamp` type, must be within the specified duration of the current time. If the field value isn't within the duration, an error message is generated.\n   *\n   * ```proto\n   * message MyTimestamp {\n   *   // value must be within 1 hour of now\n   *   google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.within = {seconds: 3600}];\n   * }\n   * ```\n   *\n   * @generated from field: optional google.protobuf.Duration within = 9;\n   */\n  within?: Duration;\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyTimestamp {\n   *   google.protobuf.Timestamp value = 1 [\n   *     (buf.validate.field).timestamp.example = { seconds: 1672444800 },\n   *     (buf.validate.field).timestamp.example = { seconds: 1672531200 },\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated google.protobuf.Timestamp example = 10;\n   */\n  example: Timestamp[];\n};\n\n/**\n * Describes the message buf.validate.TimestampRules.\n * Use `create(TimestampRulesSchema)` to create a new message.\n */\nexport const TimestampRulesSchema: GenMessage<TimestampRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 26);\n\n/**\n * `Violations` is a collection of `Violation` messages. This message type is returned by\n * Protovalidate when a proto message fails to meet the requirements set by the `Rule` validation rules.\n * Each individual violation is represented by a `Violation` message.\n *\n * @generated from message buf.validate.Violations\n */\nexport type Violations = Message<\"buf.validate.Violations\"> & {\n  /**\n   * `violations` is a repeated field that contains all the `Violation` messages corresponding to the violations detected.\n   *\n   * @generated from field: repeated buf.validate.Violation violations = 1;\n   */\n  violations: Violation[];\n};\n\n/**\n * Describes the message buf.validate.Violations.\n * Use `create(ViolationsSchema)` to create a new message.\n */\nexport const ViolationsSchema: GenMessage<Violations> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 27);\n\n/**\n * `Violation` represents a single instance where a validation rule, expressed\n * as a `Rule`, was not met. It provides information about the field that\n * caused the violation, the specific rule that wasn't fulfilled, and a\n * human-readable error message.\n *\n * For example, consider the following message:\n *\n * ```proto\n * message User {\n *     int32 age = 1 [(buf.validate.field).cel = {\n *         id: \"user.age\",\n *         expression: \"this < 18 ? 'User must be at least 18 years old' : ''\",\n *     }];\n * }\n * ```\n *\n * It could produce the following violation:\n *\n * ```json\n * {\n *   \"ruleId\": \"user.age\",\n *   \"message\": \"User must be at least 18 years old\",\n *   \"field\": {\n *     \"elements\": [\n *       {\n *         \"fieldNumber\": 1,\n *         \"fieldName\": \"age\",\n *         \"fieldType\": \"TYPE_INT32\"\n *       }\n *     ]\n *   },\n *   \"rule\": {\n *     \"elements\": [\n *       {\n *         \"fieldNumber\": 23,\n *         \"fieldName\": \"cel\",\n *         \"fieldType\": \"TYPE_MESSAGE\",\n *         \"index\": \"0\"\n *       }\n *     ]\n *   }\n * }\n * ```\n *\n * @generated from message buf.validate.Violation\n */\nexport type Violation = Message<\"buf.validate.Violation\"> & {\n  /**\n   * `field` is a machine-readable path to the field that failed validation.\n   * This could be a nested field, in which case the path will include all the parent fields leading to the actual field that caused the violation.\n   *\n   * For example, consider the following message:\n   *\n   * ```proto\n   * message Message {\n   *   bool a = 1 [(buf.validate.field).required = true];\n   * }\n   * ```\n   *\n   * It could produce the following violation:\n   *\n   * ```textproto\n   * violation {\n   *   field { element { field_number: 1, field_name: \"a\", field_type: 8 } }\n   *   ...\n   * }\n   * ```\n   *\n   * @generated from field: optional buf.validate.FieldPath field = 5;\n   */\n  field?: FieldPath;\n\n  /**\n   * `rule` is a machine-readable path that points to the specific rule that failed validation.\n   * This will be a nested field starting from the FieldRules of the field that failed validation.\n   * For custom rules, this will provide the path of the rule, e.g. `cel[0]`.\n   *\n   * For example, consider the following message:\n   *\n   * ```proto\n   * message Message {\n   *   bool a = 1 [(buf.validate.field).required = true];\n   *   bool b = 2 [(buf.validate.field).cel = {\n   *     id: \"custom_rule\",\n   *     expression: \"!this ? 'b must be true': ''\"\n   *   }]\n   * }\n   * ```\n   *\n   * It could produce the following violations:\n   *\n   * ```textproto\n   * violation {\n   *   rule { element { field_number: 25, field_name: \"required\", field_type: 8 } }\n   *   ...\n   * }\n   * violation {\n   *   rule { element { field_number: 23, field_name: \"cel\", field_type: 11, index: 0 } }\n   *   ...\n   * }\n   * ```\n   *\n   * @generated from field: optional buf.validate.FieldPath rule = 6;\n   */\n  rule?: FieldPath;\n\n  /**\n   * `rule_id` is the unique identifier of the `Rule` that was not fulfilled.\n   * This is the same `id` that was specified in the `Rule` message, allowing easy tracing of which rule was violated.\n   *\n   * @generated from field: optional string rule_id = 2;\n   */\n  ruleId: string;\n\n  /**\n   * `message` is a human-readable error message that describes the nature of the violation.\n   * This can be the default error message from the violated `Rule`, or it can be a custom message that gives more context about the violation.\n   *\n   * @generated from field: optional string message = 3;\n   */\n  message: string;\n\n  /**\n   * `for_key` indicates whether the violation was caused by a map key, rather than a value.\n   *\n   * @generated from field: optional bool for_key = 4;\n   */\n  forKey: boolean;\n};\n\n/**\n * Describes the message buf.validate.Violation.\n * Use `create(ViolationSchema)` to create a new message.\n */\nexport const ViolationSchema: GenMessage<Violation> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 28);\n\n/**\n * `FieldPath` provides a path to a nested protobuf field.\n *\n * This message provides enough information to render a dotted field path even without protobuf descriptors.\n * It also provides enough information to resolve a nested field through unknown wire data.\n *\n * @generated from message buf.validate.FieldPath\n */\nexport type FieldPath = Message<\"buf.validate.FieldPath\"> & {\n  /**\n   * `elements` contains each element of the path, starting from the root and recursing downward.\n   *\n   * @generated from field: repeated buf.validate.FieldPathElement elements = 1;\n   */\n  elements: FieldPathElement[];\n};\n\n/**\n * Describes the message buf.validate.FieldPath.\n * Use `create(FieldPathSchema)` to create a new message.\n */\nexport const FieldPathSchema: GenMessage<FieldPath> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 29);\n\n/**\n * `FieldPathElement` provides enough information to nest through a single protobuf field.\n *\n * If the selected field is a map or repeated field, the `subscript` value selects a specific element from it.\n * A path that refers to a value nested under a map key or repeated field index will have a `subscript` value.\n * The `field_type` field allows unambiguous resolution of a field even if descriptors are not available.\n *\n * @generated from message buf.validate.FieldPathElement\n */\nexport type FieldPathElement = Message<\"buf.validate.FieldPathElement\"> & {\n  /**\n   * `field_number` is the field number this path element refers to.\n   *\n   * @generated from field: optional int32 field_number = 1;\n   */\n  fieldNumber: number;\n\n  /**\n   * `field_name` contains the field name this path element refers to.\n   * This can be used to display a human-readable path even if the field number is unknown.\n   *\n   * @generated from field: optional string field_name = 2;\n   */\n  fieldName: string;\n\n  /**\n   * `field_type` specifies the type of this field. When using reflection, this value is not needed.\n   *\n   * This value is provided to make it possible to traverse unknown fields through wire data.\n   * When traversing wire data, be mindful of both packed[1] and delimited[2] encoding schemes.\n   *\n   * [1]: https://protobuf.dev/programming-guides/encoding/#packed\n   * [2]: https://protobuf.dev/programming-guides/encoding/#groups\n   *\n   * N.B.: Although groups are deprecated, the corresponding delimited encoding scheme is not, and\n   * can be explicitly used in Protocol Buffers 2023 Edition.\n   *\n   * @generated from field: optional google.protobuf.FieldDescriptorProto.Type field_type = 3;\n   */\n  fieldType: FieldDescriptorProto_Type;\n\n  /**\n   * `key_type` specifies the map key type of this field. This value is useful when traversing\n   * unknown fields through wire data: specifically, it allows handling the differences between\n   * different integer encodings.\n   *\n   * @generated from field: optional google.protobuf.FieldDescriptorProto.Type key_type = 4;\n   */\n  keyType: FieldDescriptorProto_Type;\n\n  /**\n   * `value_type` specifies map value type of this field. This is useful if you want to display a\n   * value inside unknown fields through wire data.\n   *\n   * @generated from field: optional google.protobuf.FieldDescriptorProto.Type value_type = 5;\n   */\n  valueType: FieldDescriptorProto_Type;\n\n  /**\n   * `subscript` contains a repeated index or map key, if this path element nests into a repeated or map field.\n   *\n   * @generated from oneof buf.validate.FieldPathElement.subscript\n   */\n  subscript: {\n    /**\n     * `index` specifies a 0-based index into a repeated field.\n     *\n     * @generated from field: uint64 index = 6;\n     */\n    value: bigint;\n    case: \"index\";\n  } | {\n    /**\n     * `bool_key` specifies a map key of type bool.\n     *\n     * @generated from field: bool bool_key = 7;\n     */\n    value: boolean;\n    case: \"boolKey\";\n  } | {\n    /**\n     * `int_key` specifies a map key of type int32, int64, sint32, sint64, sfixed32 or sfixed64.\n     *\n     * @generated from field: int64 int_key = 8;\n     */\n    value: bigint;\n    case: \"intKey\";\n  } | {\n    /**\n     * `uint_key` specifies a map key of type uint32, uint64, fixed32 or fixed64.\n     *\n     * @generated from field: uint64 uint_key = 9;\n     */\n    value: bigint;\n    case: \"uintKey\";\n  } | {\n    /**\n     * `string_key` specifies a map key of type string.\n     *\n     * @generated from field: string string_key = 10;\n     */\n    value: string;\n    case: \"stringKey\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message buf.validate.FieldPathElement.\n * Use `create(FieldPathElementSchema)` to create a new message.\n */\nexport const FieldPathElementSchema: GenMessage<FieldPathElement> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 30);\n\n/**\n * Specifies how `FieldRules.ignore` behaves, depending on the field's value, and\n * whether the field tracks presence.\n *\n * @generated from enum buf.validate.Ignore\n */\nexport enum Ignore {\n  /**\n   * Ignore rules if the field tracks presence and is unset. This is the default\n   * behavior.\n   *\n   * In proto3, only message fields, members of a Protobuf `oneof`, and fields\n   * with the `optional` label track presence. Consequently, the following fields\n   * are always validated, whether a value is set or not:\n   *\n   * ```proto\n   * syntax=\"proto3\";\n   *\n   * message RulesApply {\n   *   string email = 1 [\n   *     (buf.validate.field).string.email = true\n   *   ];\n   *   int32 age = 2 [\n   *     (buf.validate.field).int32.gt = 0\n   *   ];\n   *   repeated string labels = 3 [\n   *     (buf.validate.field).repeated.min_items = 1\n   *   ];\n   * }\n   * ```\n   *\n   * In contrast, the following fields track presence, and are only validated if\n   * a value is set:\n   *\n   * ```proto\n   * syntax=\"proto3\";\n   *\n   * message RulesApplyIfSet {\n   *   optional string email = 1 [\n   *     (buf.validate.field).string.email = true\n   *   ];\n   *   oneof ref {\n   *     string reference = 2 [\n   *       (buf.validate.field).string.uuid = true\n   *     ];\n   *     string name = 3 [\n   *       (buf.validate.field).string.min_len = 4\n   *     ];\n   *   }\n   *   SomeMessage msg = 4 [\n   *     (buf.validate.field).cel = {/* ... *\\/}\n   *   ];\n   * }\n   * ```\n   *\n   * To ensure that such a field is set, add the `required` rule.\n   *\n   * To learn which fields track presence, see the\n   * [Field Presence cheat sheet](https://protobuf.dev/programming-guides/field_presence/#cheat).\n   *\n   * @generated from enum value: IGNORE_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * Ignore rules if the field is unset, or set to the zero value.\n   *\n   * The zero value depends on the field type:\n   * - For strings, the zero value is the empty string.\n   * - For bytes, the zero value is empty bytes.\n   * - For bool, the zero value is false.\n   * - For numeric types, the zero value is zero.\n   * - For enums, the zero value is the first defined enum value.\n   * - For repeated fields, the zero is an empty list.\n   * - For map fields, the zero is an empty map.\n   * - For message fields, absence of the message (typically a null-value) is considered zero value.\n   *\n   * For fields that track presence (e.g. adding the `optional` label in proto3),\n   * this a no-op and behavior is the same as the default `IGNORE_UNSPECIFIED`.\n   *\n   * @generated from enum value: IGNORE_IF_ZERO_VALUE = 1;\n   */\n  IF_ZERO_VALUE = 1,\n\n  /**\n   * Always ignore rules, including the `required` rule.\n   *\n   * This is useful for ignoring the rules of a referenced message, or to\n   * temporarily ignore rules during development.\n   *\n   * ```proto\n   * message MyMessage {\n   *   // The field's rules will always be ignored, including any validations\n   *   // on value's fields.\n   *   MyOtherMessage value = 1 [\n   *     (buf.validate.field).ignore = IGNORE_ALWAYS\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from enum value: IGNORE_ALWAYS = 3;\n   */\n  ALWAYS = 3,\n}\n\n/**\n * Describes the enum buf.validate.Ignore.\n */\nexport const IgnoreSchema: GenEnum<Ignore> = /*@__PURE__*/\n  enumDesc(file_buf_validate_validate, 0);\n\n/**\n * KnownRegex contains some well-known patterns.\n *\n * @generated from enum buf.validate.KnownRegex\n */\nexport enum KnownRegex {\n  /**\n   * @generated from enum value: KNOWN_REGEX_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * HTTP header name as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2).\n   *\n   * @generated from enum value: KNOWN_REGEX_HTTP_HEADER_NAME = 1;\n   */\n  HTTP_HEADER_NAME = 1,\n\n  /**\n   * HTTP header value as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4).\n   *\n   * @generated from enum value: KNOWN_REGEX_HTTP_HEADER_VALUE = 2;\n   */\n  HTTP_HEADER_VALUE = 2,\n}\n\n/**\n * Describes the enum buf.validate.KnownRegex.\n */\nexport const KnownRegexSchema: GenEnum<KnownRegex> = /*@__PURE__*/\n  enumDesc(file_buf_validate_validate, 1);\n\n/**\n * Rules specify the validations to be performed on this message. By default,\n * no validation is performed against a message.\n *\n * @generated from extension: optional buf.validate.MessageRules message = 1159;\n */\nexport const message: GenExtension<MessageOptions, MessageRules> = /*@__PURE__*/\n  extDesc(file_buf_validate_validate, 0);\n\n/**\n * Rules specify the validations to be performed on this oneof. By default,\n * no validation is performed against a oneof.\n *\n * @generated from extension: optional buf.validate.OneofRules oneof = 1159;\n */\nexport const oneof: GenExtension<OneofOptions, OneofRules> = /*@__PURE__*/\n  extDesc(file_buf_validate_validate, 1);\n\n/**\n * Rules specify the validations to be performed on this field. By default,\n * no validation is performed against a field.\n *\n * @generated from extension: optional buf.validate.FieldRules field = 1159;\n */\nexport const field: GenExtension<FieldOptions, FieldRules> = /*@__PURE__*/\n  extDesc(file_buf_validate_validate, 2);\n\n/**\n * Specifies predefined rules. When extending a standard rule message,\n * this adds additional CEL expressions that apply when the extension is used.\n *\n * ```proto\n * extend buf.validate.Int32Rules {\n *   bool is_zero [(buf.validate.predefined).cel = {\n *     id: \"int32.is_zero\",\n *     message: \"value must be zero\",\n *     expression: \"!rule || this == 0\",\n *   }];\n * }\n *\n * message Foo {\n *   int32 reserved = 1 [(buf.validate.field).int32.(is_zero) = true];\n * }\n * ```\n *\n * @generated from extension: optional buf.validate.PredefinedRules predefined = 1160;\n */\nexport const predefined: GenExtension<FieldOptions, PredefinedRules> = /*@__PURE__*/\n  extDesc(file_buf_validate_validate, 3);\n\n"
  },
  {
    "path": "app/generated/ito_connect.ts",
    "content": "// @generated by protoc-gen-connect-es v1.6.1 with parameter \"target=ts,import_extension=.js\"\n// @generated from file ito.proto (package ito, syntax proto3)\n/* eslint-disable */\n// @ts-nocheck\n\nimport { AdvancedSettings, AudioChunk, CreateDictionaryItemRequest, CreateInteractionRequest, CreateNoteRequest, DeleteDictionaryItemRequest, DeleteInteractionRequest, DeleteNoteRequest, DeleteUserDataRequest, DictionaryItem, Empty, GetAdvancedSettingsRequest, GetInteractionRequest, GetNoteRequest, Interaction, ListDictionaryItemsRequest, ListDictionaryItemsResponse, ListInteractionsRequest, ListInteractionsResponse, ListNotesRequest, ListNotesResponse, Note, SubmitTimingReportsRequest, SubmitTimingReportsResponse, TranscribeStreamRequest, TranscriptionResponse, UpdateAdvancedSettingsRequest, UpdateDictionaryItemRequest, UpdateInteractionRequest, UpdateNoteRequest } from \"./ito_pb.js\";\nimport { MethodKind } from \"@bufbuild/protobuf\";\n\n/**\n * @generated from service ito.ItoService\n */\nexport const ItoService = {\n  typeName: \"ito.ItoService\",\n  methods: {\n    /**\n     * Streams audio chunks from the client and gets a single response.\n     * This is the ideal method for dictation to reduce latency and memory usage.\n     *\n     * @generated from rpc ito.ItoService.TranscribeStream\n     */\n    transcribeStream: {\n      name: \"TranscribeStream\",\n      I: AudioChunk,\n      O: TranscriptionResponse,\n      kind: MethodKind.ClientStreaming,\n    },\n    /**\n     * Enhanced streaming transcription that accepts configuration data in-stream.\n     * Config can be sent before, during, or omitted entirely. Multiple config messages\n     * are merged by the server. This allows immediate streaming without waiting for context.\n     *\n     * @generated from rpc ito.ItoService.TranscribeStreamV2\n     */\n    transcribeStreamV2: {\n      name: \"TranscribeStreamV2\",\n      I: TranscribeStreamRequest,\n      O: TranscriptionResponse,\n      kind: MethodKind.ClientStreaming,\n    },\n    /**\n     * Note Service\n     *\n     * @generated from rpc ito.ItoService.CreateNote\n     */\n    createNote: {\n      name: \"CreateNote\",\n      I: CreateNoteRequest,\n      O: Note,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.GetNote\n     */\n    getNote: {\n      name: \"GetNote\",\n      I: GetNoteRequest,\n      O: Note,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.ListNotes\n     */\n    listNotes: {\n      name: \"ListNotes\",\n      I: ListNotesRequest,\n      O: ListNotesResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.UpdateNote\n     */\n    updateNote: {\n      name: \"UpdateNote\",\n      I: UpdateNoteRequest,\n      O: Note,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.DeleteNote\n     */\n    deleteNote: {\n      name: \"DeleteNote\",\n      I: DeleteNoteRequest,\n      O: Empty,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Interaction Service\n     *\n     * @generated from rpc ito.ItoService.CreateInteraction\n     */\n    createInteraction: {\n      name: \"CreateInteraction\",\n      I: CreateInteractionRequest,\n      O: Interaction,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.GetInteraction\n     */\n    getInteraction: {\n      name: \"GetInteraction\",\n      I: GetInteractionRequest,\n      O: Interaction,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.ListInteractions\n     */\n    listInteractions: {\n      name: \"ListInteractions\",\n      I: ListInteractionsRequest,\n      O: ListInteractionsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.UpdateInteraction\n     */\n    updateInteraction: {\n      name: \"UpdateInteraction\",\n      I: UpdateInteractionRequest,\n      O: Interaction,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.DeleteInteraction\n     */\n    deleteInteraction: {\n      name: \"DeleteInteraction\",\n      I: DeleteInteractionRequest,\n      O: Empty,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Dictionary Service\n     *\n     * @generated from rpc ito.ItoService.CreateDictionaryItem\n     */\n    createDictionaryItem: {\n      name: \"CreateDictionaryItem\",\n      I: CreateDictionaryItemRequest,\n      O: DictionaryItem,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.ListDictionaryItems\n     */\n    listDictionaryItems: {\n      name: \"ListDictionaryItems\",\n      I: ListDictionaryItemsRequest,\n      O: ListDictionaryItemsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.UpdateDictionaryItem\n     */\n    updateDictionaryItem: {\n      name: \"UpdateDictionaryItem\",\n      I: UpdateDictionaryItemRequest,\n      O: DictionaryItem,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.DeleteDictionaryItem\n     */\n    deleteDictionaryItem: {\n      name: \"DeleteDictionaryItem\",\n      I: DeleteDictionaryItemRequest,\n      O: Empty,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * User Data Service\n     *\n     * @generated from rpc ito.ItoService.DeleteUserData\n     */\n    deleteUserData: {\n      name: \"DeleteUserData\",\n      I: DeleteUserDataRequest,\n      O: Empty,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Advanced Settings Service\n     *\n     * @generated from rpc ito.ItoService.GetAdvancedSettings\n     */\n    getAdvancedSettings: {\n      name: \"GetAdvancedSettings\",\n      I: GetAdvancedSettingsRequest,\n      O: AdvancedSettings,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.UpdateAdvancedSettings\n     */\n    updateAdvancedSettings: {\n      name: \"UpdateAdvancedSettings\",\n      I: UpdateAdvancedSettingsRequest,\n      O: AdvancedSettings,\n      kind: MethodKind.Unary,\n    },\n  }\n} as const;\n\n/**\n * @generated from service ito.TimingService\n */\nexport const TimingService = {\n  typeName: \"ito.TimingService\",\n  methods: {\n    /**\n     * Submit timing reports for interaction analytics\n     *\n     * @generated from rpc ito.TimingService.SubmitTimingReports\n     */\n    submitTimingReports: {\n      name: \"SubmitTimingReports\",\n      I: SubmitTimingReportsRequest,\n      O: SubmitTimingReportsResponse,\n      kind: MethodKind.Unary,\n    },\n  }\n} as const;\n\n"
  },
  {
    "path": "app/generated/ito_pb.ts",
    "content": "// @generated by protoc-gen-es v2.7.0 with parameter \"target=ts,import_extension=.js\"\n// @generated from file ito.proto (package ito, syntax proto3)\n/* eslint-disable */\n\nimport type { GenEnum, GenFile, GenMessage, GenService } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, fileDesc, messageDesc, serviceDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport { file_buf_validate_validate } from \"./buf/validate/validate_pb.js\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file ito.proto.\n */\nexport const file_ito: GenFile = /*@__PURE__*/\n  fileDesc(\"CglpdG8ucHJvdG8SA2l0byIHCgVFbXB0eSLRAQoLQ2xpZW50RXJyb3ISDAoEY29kZRgBIAEoCRIcCgR0eXBlGAIgASgOMg4uaXRvLkVycm9yVHlwZRIPCgdtZXNzYWdlGAMgASgJEiUKCHByb3ZpZGVyGAQgASgOMhMuaXRvLkNsaWVudFByb3ZpZGVyEi4KB2RldGFpbHMYBSADKAsyHS5pdG8uQ2xpZW50RXJyb3IuRGV0YWlsc0VudHJ5Gi4KDERldGFpbHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIisKCkF1ZGlvQ2h1bmsSHQoKYXVkaW9fZGF0YRgBIAEoDEIJukgGegQYgIBAIrMBCgtDb250ZXh0SW5mbxIZCgx3aW5kb3dfdGl0bGUYASABKAlIAIgBARIVCghhcHBfbmFtZRgCIAEoCUgBiAEBEhkKDGNvbnRleHRfdGV4dBgDIAEoCUgCiAEBEh8KBG1vZGUYBCABKA4yDC5pdG8uSXRvTW9kZUgDiAEBQg8KDV93aW5kb3dfdGl0bGVCCwoJX2FwcF9uYW1lQg8KDV9jb250ZXh0X3RleHRCBwoFX21vZGUixAEKDFN0cmVhbUNvbmZpZxImCgdjb250ZXh0GAEgASgLMhAuaXRvLkNvbnRleHRJbmZvSACIAQESKwoMbGxtX3NldHRpbmdzGAIgASgLMhAuaXRvLkxsbVNldHRpbmdzSAGIAQESEgoKdm9jYWJ1bGFyeRgDIAMoCRIbCg5pbnRlcmFjdGlvbl9pZBgEIAEoCUgCiAEBQgoKCF9jb250ZXh0Qg8KDV9sbG1fc2V0dGluZ3NCEQoPX2ludGVyYWN0aW9uX2lkImoKF1RyYW5zY3JpYmVTdHJlYW1SZXF1ZXN0EiMKBmNvbmZpZxgBIAEoCzIRLml0by5TdHJlYW1Db25maWdIABIfCgphdWRpb19kYXRhGAIgASgMQgm6SAZ6BBiAgEBIAEIJCgdwYXlsb2FkIkwKFVRyYW5zY3JpcHRpb25SZXNwb25zZRISCgp0cmFuc2NyaXB0GAEgASgJEh8KBWVycm9yGAIgASgLMhAuaXRvLkNsaWVudEVycm9yIogBCgROb3RlEgoKAmlkGAEgASgJEg8KB3VzZXJfaWQYAiABKAkSFgoOaW50ZXJhY3Rpb25faWQYAyABKAkSDwoHY29udGVudBgEIAEoCRISCgpjcmVhdGVkX2F0GAUgASgJEhIKCnVwZGF0ZWRfYXQYBiABKAkSEgoKZGVsZXRlZF9hdBgHIAEoCSJIChFDcmVhdGVOb3RlUmVxdWVzdBIKCgJpZBgBIAEoCRIWCg5pbnRlcmFjdGlvbl9pZBgCIAEoCRIPCgdjb250ZW50GAMgASgJIhwKDkdldE5vdGVSZXF1ZXN0EgoKAmlkGAEgASgJIisKEExpc3ROb3Rlc1JlcXVlc3QSFwoPc2luY2VfdGltZXN0YW1wGAEgASgJIi0KEUxpc3ROb3Rlc1Jlc3BvbnNlEhgKBW5vdGVzGAEgAygLMgkuaXRvLk5vdGUiMAoRVXBkYXRlTm90ZVJlcXVlc3QSCgoCaWQYASABKAkSDwoHY29udGVudBgCIAEoCSIfChFEZWxldGVOb3RlUmVxdWVzdBIKCgJpZBgBIAEoCSL9AQoLSW50ZXJhY3Rpb24SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRINCgV0aXRsZRgDIAEoCRISCgphc3Jfb3V0cHV0GAQgASgJEhIKCmxsbV9vdXRwdXQYBSABKAkSHQoJcmF3X2F1ZGlvGAYgASgMQgq6SAd6BRiAwtcvEhMKC2R1cmF0aW9uX21zGAcgASgFEhIKCmNyZWF0ZWRfYXQYCCABKAkSEgoKdXBkYXRlZF9hdBgJIAEoCRISCgpkZWxldGVkX2F0GAogASgJEhkKDHJhd19hdWRpb19pZBgLIAEoCUgAiAEBQg8KDV9yYXdfYXVkaW9faWQikQEKGENyZWF0ZUludGVyYWN0aW9uUmVxdWVzdBIKCgJpZBgBIAEoCRINCgV0aXRsZRgCIAEoCRISCgphc3Jfb3V0cHV0GAMgASgJEhIKCmxsbV9vdXRwdXQYBCABKAkSHQoJcmF3X2F1ZGlvGAUgASgMQgq6SAd6BRiAwtcvEhMKC2R1cmF0aW9uX21zGAYgASgFIiMKFUdldEludGVyYWN0aW9uUmVxdWVzdBIKCgJpZBgBIAEoCSIyChdMaXN0SW50ZXJhY3Rpb25zUmVxdWVzdBIXCg9zaW5jZV90aW1lc3RhbXAYASABKAkiQgoYTGlzdEludGVyYWN0aW9uc1Jlc3BvbnNlEiYKDGludGVyYWN0aW9ucxgBIAMoCzIQLml0by5JbnRlcmFjdGlvbiI1ChhVcGRhdGVJbnRlcmFjdGlvblJlcXVlc3QSCgoCaWQYASABKAkSDQoFdGl0bGUYAiABKAkiJgoYRGVsZXRlSW50ZXJhY3Rpb25SZXF1ZXN0EgoKAmlkGAEgASgJIo4BCg5EaWN0aW9uYXJ5SXRlbRIKCgJpZBgBIAEoCRIPCgd1c2VyX2lkGAIgASgJEgwKBHdvcmQYAyABKAkSFQoNcHJvbnVuY2lhdGlvbhgEIAEoCRISCgpjcmVhdGVkX2F0GAUgASgJEhIKCnVwZGF0ZWRfYXQYBiABKAkSEgoKZGVsZXRlZF9hdBgHIAEoCSJOChtDcmVhdGVEaWN0aW9uYXJ5SXRlbVJlcXVlc3QSCgoCaWQYASABKAkSDAoEd29yZBgCIAEoCRIVCg1wcm9udW5jaWF0aW9uGAMgASgJIjUKGkxpc3REaWN0aW9uYXJ5SXRlbXNSZXF1ZXN0EhcKD3NpbmNlX3RpbWVzdGFtcBgBIAEoCSJBChtMaXN0RGljdGlvbmFyeUl0ZW1zUmVzcG9uc2USIgoFaXRlbXMYASADKAsyEy5pdG8uRGljdGlvbmFyeUl0ZW0iTgobVXBkYXRlRGljdGlvbmFyeUl0ZW1SZXF1ZXN0EgoKAmlkGAEgASgJEgwKBHdvcmQYAiABKAkSFQoNcHJvbnVuY2lhdGlvbhgDIAEoCSIpChtEZWxldGVEaWN0aW9uYXJ5SXRlbVJlcXVlc3QSCgoCaWQYASABKAkiFwoVRGVsZXRlVXNlckRhdGFSZXF1ZXN0Iu8DCgtMbG1TZXR0aW5ncxIWCglhc3JfbW9kZWwYASABKAlIAIgBARIZCgxhc3JfcHJvdmlkZXIYAiABKAlIAYgBARIXCgphc3JfcHJvbXB0GAMgASgJSAKIAQESGQoMbGxtX3Byb3ZpZGVyGAQgASgJSAOIAQESFgoJbGxtX21vZGVsGAUgASgJSASIAQESHAoPbGxtX3RlbXBlcmF0dXJlGAYgASgCSAWIAQESIQoUdHJhbnNjcmlwdGlvbl9wcm9tcHQYByABKAlIBogBARIbCg5lZGl0aW5nX3Byb21wdBgIIAEoCUgHiAEBEiAKE25vX3NwZWVjaF90aHJlc2hvbGQYCSABKAJICIgBARIiChVsb3dfcXVhbGl0eV90aHJlc2hvbGQYCiABKAJICYgBAUIMCgpfYXNyX21vZGVsQg8KDV9hc3JfcHJvdmlkZXJCDQoLX2Fzcl9wcm9tcHRCDwoNX2xsbV9wcm92aWRlckIMCgpfbGxtX21vZGVsQhIKEF9sbG1fdGVtcGVyYXR1cmVCFwoVX3RyYW5zY3JpcHRpb25fcHJvbXB0QhEKD19lZGl0aW5nX3Byb21wdEIWChRfbm9fc3BlZWNoX3RocmVzaG9sZEIYChZfbG93X3F1YWxpdHlfdGhyZXNob2xkIpkBChBBZHZhbmNlZFNldHRpbmdzEgoKAmlkGAEgASgJEg8KB3VzZXJfaWQYAiABKAkSEgoKY3JlYXRlZF9hdBgDIAEoCRISCgp1cGRhdGVkX2F0GAQgASgJEh0KA2xsbRgFIAEoCzIQLml0by5MbG1TZXR0aW5ncxIhCgdkZWZhdWx0GAYgASgLMhAuaXRvLkxsbVNldHRpbmdzIhwKGkdldEFkdmFuY2VkU2V0dGluZ3NSZXF1ZXN0Ij4KHVVwZGF0ZUFkdmFuY2VkU2V0dGluZ3NSZXF1ZXN0Eh0KA2xsbRgBIAEoCzIQLml0by5MbG1TZXR0aW5ncyJ3CgtUaW1pbmdFdmVudBIMCgRuYW1lGAEgASgJEhAKCHN0YXJ0X21zGAIgASgBEhMKBmVuZF9tcxgDIAEoAUgAiAEBEhgKC2R1cmF0aW9uX21zGAQgASgBSAGIAQFCCQoHX2VuZF9tc0IOCgxfZHVyYXRpb25fbXMi1gEKDFRpbWluZ1JlcG9ydBIWCg5pbnRlcmFjdGlvbl9pZBgBIAEoCRIPCgd1c2VyX2lkGAIgASgJEhAKCHBsYXRmb3JtGAMgASgJEhMKC2FwcF92ZXJzaW9uGAQgASgJEhAKCGhvc3RuYW1lGAUgASgJEhQKDGFyY2hpdGVjdHVyZRgGIAEoCRIRCgl0aW1lc3RhbXAYByABKAkSIAoGZXZlbnRzGAggAygLMhAuaXRvLlRpbWluZ0V2ZW50EhkKEXRvdGFsX2R1cmF0aW9uX21zGAkgASgBIkAKGlN1Ym1pdFRpbWluZ1JlcG9ydHNSZXF1ZXN0EiIKB3JlcG9ydHMYASADKAsyES5pdG8uVGltaW5nUmVwb3J0Ih0KG1N1Ym1pdFRpbWluZ1JlcG9ydHNSZXNwb25zZSojCgdJdG9Nb2RlEg4KClRSQU5TQ1JJQkUQABIICgRFRElUEAEqKAoOQ2xpZW50UHJvdmlkZXISCAoER1JPURAAEgwKCENFUkVCUkFTEAEqRAoJRXJyb3JUeXBlEhEKDUNPTkZJR1VSQVRJT04QABIQCgxBVkFJTEFCSUxJVFkQARIJCgVBVURJTxACEgcKA0FQSRADMpUKCgpJdG9TZXJ2aWNlEkEKEFRyYW5zY3JpYmVTdHJlYW0SDy5pdG8uQXVkaW9DaHVuaxoaLml0by5UcmFuc2NyaXB0aW9uUmVzcG9uc2UoARJQChJUcmFuc2NyaWJlU3RyZWFtVjISHC5pdG8uVHJhbnNjcmliZVN0cmVhbVJlcXVlc3QaGi5pdG8uVHJhbnNjcmlwdGlvblJlc3BvbnNlKAESLwoKQ3JlYXRlTm90ZRIWLml0by5DcmVhdGVOb3RlUmVxdWVzdBoJLml0by5Ob3RlEikKB0dldE5vdGUSEy5pdG8uR2V0Tm90ZVJlcXVlc3QaCS5pdG8uTm90ZRI6CglMaXN0Tm90ZXMSFS5pdG8uTGlzdE5vdGVzUmVxdWVzdBoWLml0by5MaXN0Tm90ZXNSZXNwb25zZRIvCgpVcGRhdGVOb3RlEhYuaXRvLlVwZGF0ZU5vdGVSZXF1ZXN0GgkuaXRvLk5vdGUSMAoKRGVsZXRlTm90ZRIWLml0by5EZWxldGVOb3RlUmVxdWVzdBoKLml0by5FbXB0eRJEChFDcmVhdGVJbnRlcmFjdGlvbhIdLml0by5DcmVhdGVJbnRlcmFjdGlvblJlcXVlc3QaEC5pdG8uSW50ZXJhY3Rpb24SPgoOR2V0SW50ZXJhY3Rpb24SGi5pdG8uR2V0SW50ZXJhY3Rpb25SZXF1ZXN0GhAuaXRvLkludGVyYWN0aW9uEk8KEExpc3RJbnRlcmFjdGlvbnMSHC5pdG8uTGlzdEludGVyYWN0aW9uc1JlcXVlc3QaHS5pdG8uTGlzdEludGVyYWN0aW9uc1Jlc3BvbnNlEkQKEVVwZGF0ZUludGVyYWN0aW9uEh0uaXRvLlVwZGF0ZUludGVyYWN0aW9uUmVxdWVzdBoQLml0by5JbnRlcmFjdGlvbhI+ChFEZWxldGVJbnRlcmFjdGlvbhIdLml0by5EZWxldGVJbnRlcmFjdGlvblJlcXVlc3QaCi5pdG8uRW1wdHkSTQoUQ3JlYXRlRGljdGlvbmFyeUl0ZW0SIC5pdG8uQ3JlYXRlRGljdGlvbmFyeUl0ZW1SZXF1ZXN0GhMuaXRvLkRpY3Rpb25hcnlJdGVtElgKE0xpc3REaWN0aW9uYXJ5SXRlbXMSHy5pdG8uTGlzdERpY3Rpb25hcnlJdGVtc1JlcXVlc3QaIC5pdG8uTGlzdERpY3Rpb25hcnlJdGVtc1Jlc3BvbnNlEk0KFFVwZGF0ZURpY3Rpb25hcnlJdGVtEiAuaXRvLlVwZGF0ZURpY3Rpb25hcnlJdGVtUmVxdWVzdBoTLml0by5EaWN0aW9uYXJ5SXRlbRJEChREZWxldGVEaWN0aW9uYXJ5SXRlbRIgLml0by5EZWxldGVEaWN0aW9uYXJ5SXRlbVJlcXVlc3QaCi5pdG8uRW1wdHkSOAoORGVsZXRlVXNlckRhdGESGi5pdG8uRGVsZXRlVXNlckRhdGFSZXF1ZXN0GgouaXRvLkVtcHR5Ek0KE0dldEFkdmFuY2VkU2V0dGluZ3MSHy5pdG8uR2V0QWR2YW5jZWRTZXR0aW5nc1JlcXVlc3QaFS5pdG8uQWR2YW5jZWRTZXR0aW5ncxJTChZVcGRhdGVBZHZhbmNlZFNldHRpbmdzEiIuaXRvLlVwZGF0ZUFkdmFuY2VkU2V0dGluZ3NSZXF1ZXN0GhUuaXRvLkFkdmFuY2VkU2V0dGluZ3MyaQoNVGltaW5nU2VydmljZRJYChNTdWJtaXRUaW1pbmdSZXBvcnRzEh8uaXRvLlN1Ym1pdFRpbWluZ1JlcG9ydHNSZXF1ZXN0GiAuaXRvLlN1Ym1pdFRpbWluZ1JlcG9ydHNSZXNwb25zZWIGcHJvdG8z\", [file_buf_validate_validate]);\n\n/**\n * General\n * -----------------------------------------------------------------\n *\n * @generated from message ito.Empty\n */\nexport type Empty = Message<\"ito.Empty\"> & {\n};\n\n/**\n * Describes the message ito.Empty.\n * Use `create(EmptySchema)` to create a new message.\n */\nexport const EmptySchema: GenMessage<Empty> = /*@__PURE__*/\n  messageDesc(file_ito, 0);\n\n/**\n * @generated from message ito.ClientError\n */\nexport type ClientError = Message<\"ito.ClientError\"> & {\n  /**\n   * @generated from field: string code = 1;\n   */\n  code: string;\n\n  /**\n   * @generated from field: ito.ErrorType type = 2;\n   */\n  type: ErrorType;\n\n  /**\n   * @generated from field: string message = 3;\n   */\n  message: string;\n\n  /**\n   * @generated from field: ito.ClientProvider provider = 4;\n   */\n  provider: ClientProvider;\n\n  /**\n   * @generated from field: map<string, string> details = 5;\n   */\n  details: { [key: string]: string };\n};\n\n/**\n * Describes the message ito.ClientError.\n * Use `create(ClientErrorSchema)` to create a new message.\n */\nexport const ClientErrorSchema: GenMessage<ClientError> = /*@__PURE__*/\n  messageDesc(file_ito, 1);\n\n/**\n * Transcription\n * -----------------------------------------------------------------\n * A chunk of audio data for streaming.\n *\n * @generated from message ito.AudioChunk\n */\nexport type AudioChunk = Message<\"ito.AudioChunk\"> & {\n  /**\n   * 1 MB limit per chunk\n   *\n   * @generated from field: bytes audio_data = 1;\n   */\n  audioData: Uint8Array;\n};\n\n/**\n * Describes the message ito.AudioChunk.\n * Use `create(AudioChunkSchema)` to create a new message.\n */\nexport const AudioChunkSchema: GenMessage<AudioChunk> = /*@__PURE__*/\n  messageDesc(file_ito, 2);\n\n/**\n * Context information for transcription.\n *\n * @generated from message ito.ContextInfo\n */\nexport type ContextInfo = Message<\"ito.ContextInfo\"> & {\n  /**\n   * @generated from field: optional string window_title = 1;\n   */\n  windowTitle?: string;\n\n  /**\n   * @generated from field: optional string app_name = 2;\n   */\n  appName?: string;\n\n  /**\n   * @generated from field: optional string context_text = 3;\n   */\n  contextText?: string;\n\n  /**\n   * @generated from field: optional ito.ItoMode mode = 4;\n   */\n  mode?: ItoMode;\n};\n\n/**\n * Describes the message ito.ContextInfo.\n * Use `create(ContextInfoSchema)` to create a new message.\n */\nexport const ContextInfoSchema: GenMessage<ContextInfo> = /*@__PURE__*/\n  messageDesc(file_ito, 3);\n\n/**\n * Configuration that can be sent in-stream for TranscribeStreamV2.\n * All fields are optional and will be merged by the server.\n * Multiple config messages received during the stream are progressively merged.\n *\n * @generated from message ito.StreamConfig\n */\nexport type StreamConfig = Message<\"ito.StreamConfig\"> & {\n  /**\n   * @generated from field: optional ito.ContextInfo context = 1;\n   */\n  context?: ContextInfo;\n\n  /**\n   * @generated from field: optional ito.LlmSettings llm_settings = 2;\n   */\n  llmSettings?: LlmSettings;\n\n  /**\n   * @generated from field: repeated string vocabulary = 3;\n   */\n  vocabulary: string[];\n\n  /**\n   * @generated from field: optional string interaction_id = 4;\n   */\n  interactionId?: string;\n};\n\n/**\n * Describes the message ito.StreamConfig.\n * Use `create(StreamConfigSchema)` to create a new message.\n */\nexport const StreamConfigSchema: GenMessage<StreamConfig> = /*@__PURE__*/\n  messageDesc(file_ito, 4);\n\n/**\n * Request message for TranscribeStreamV2.\n * Can contain either configuration or audio data.\n *\n * @generated from message ito.TranscribeStreamRequest\n */\nexport type TranscribeStreamRequest = Message<\"ito.TranscribeStreamRequest\"> & {\n  /**\n   * @generated from oneof ito.TranscribeStreamRequest.payload\n   */\n  payload: {\n    /**\n     * Configuration/context data\n     *\n     * @generated from field: ito.StreamConfig config = 1;\n     */\n    value: StreamConfig;\n    case: \"config\";\n  } | {\n    /**\n     * Audio chunk (1 MB limit)\n     *\n     * @generated from field: bytes audio_data = 2;\n     */\n    value: Uint8Array;\n    case: \"audioData\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message ito.TranscribeStreamRequest.\n * Use `create(TranscribeStreamRequestSchema)` to create a new message.\n */\nexport const TranscribeStreamRequestSchema: GenMessage<TranscribeStreamRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 5);\n\n/**\n * The response message containing the final transcript.\n *\n * @generated from message ito.TranscriptionResponse\n */\nexport type TranscriptionResponse = Message<\"ito.TranscriptionResponse\"> & {\n  /**\n   * @generated from field: string transcript = 1;\n   */\n  transcript: string;\n\n  /**\n   * @generated from field: ito.ClientError error = 2;\n   */\n  error?: ClientError;\n};\n\n/**\n * Describes the message ito.TranscriptionResponse.\n * Use `create(TranscriptionResponseSchema)` to create a new message.\n */\nexport const TranscriptionResponseSchema: GenMessage<TranscriptionResponse> = /*@__PURE__*/\n  messageDesc(file_ito, 6);\n\n/**\n * Notes\n * -----------------------------------------------------------------\n *\n * @generated from message ito.Note\n */\nexport type Note = Message<\"ito.Note\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * @generated from field: string user_id = 2;\n   */\n  userId: string;\n\n  /**\n   * @generated from field: string interaction_id = 3;\n   */\n  interactionId: string;\n\n  /**\n   * @generated from field: string content = 4;\n   */\n  content: string;\n\n  /**\n   * @generated from field: string created_at = 5;\n   */\n  createdAt: string;\n\n  /**\n   * @generated from field: string updated_at = 6;\n   */\n  updatedAt: string;\n\n  /**\n   * @generated from field: string deleted_at = 7;\n   */\n  deletedAt: string;\n};\n\n/**\n * Describes the message ito.Note.\n * Use `create(NoteSchema)` to create a new message.\n */\nexport const NoteSchema: GenMessage<Note> = /*@__PURE__*/\n  messageDesc(file_ito, 7);\n\n/**\n * @generated from message ito.CreateNoteRequest\n */\nexport type CreateNoteRequest = Message<\"ito.CreateNoteRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * @generated from field: string interaction_id = 2;\n   */\n  interactionId: string;\n\n  /**\n   * @generated from field: string content = 3;\n   */\n  content: string;\n};\n\n/**\n * Describes the message ito.CreateNoteRequest.\n * Use `create(CreateNoteRequestSchema)` to create a new message.\n */\nexport const CreateNoteRequestSchema: GenMessage<CreateNoteRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 8);\n\n/**\n * @generated from message ito.GetNoteRequest\n */\nexport type GetNoteRequest = Message<\"ito.GetNoteRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message ito.GetNoteRequest.\n * Use `create(GetNoteRequestSchema)` to create a new message.\n */\nexport const GetNoteRequestSchema: GenMessage<GetNoteRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 9);\n\n/**\n * @generated from message ito.ListNotesRequest\n */\nexport type ListNotesRequest = Message<\"ito.ListNotesRequest\"> & {\n  /**\n   * Optional. ISO 8601 format. If not provided, fetch all.\n   *\n   * @generated from field: string since_timestamp = 1;\n   */\n  sinceTimestamp: string;\n};\n\n/**\n * Describes the message ito.ListNotesRequest.\n * Use `create(ListNotesRequestSchema)` to create a new message.\n */\nexport const ListNotesRequestSchema: GenMessage<ListNotesRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 10);\n\n/**\n * @generated from message ito.ListNotesResponse\n */\nexport type ListNotesResponse = Message<\"ito.ListNotesResponse\"> & {\n  /**\n   * @generated from field: repeated ito.Note notes = 1;\n   */\n  notes: Note[];\n};\n\n/**\n * Describes the message ito.ListNotesResponse.\n * Use `create(ListNotesResponseSchema)` to create a new message.\n */\nexport const ListNotesResponseSchema: GenMessage<ListNotesResponse> = /*@__PURE__*/\n  messageDesc(file_ito, 11);\n\n/**\n * @generated from message ito.UpdateNoteRequest\n */\nexport type UpdateNoteRequest = Message<\"ito.UpdateNoteRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * @generated from field: string content = 2;\n   */\n  content: string;\n};\n\n/**\n * Describes the message ito.UpdateNoteRequest.\n * Use `create(UpdateNoteRequestSchema)` to create a new message.\n */\nexport const UpdateNoteRequestSchema: GenMessage<UpdateNoteRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 12);\n\n/**\n * @generated from message ito.DeleteNoteRequest\n */\nexport type DeleteNoteRequest = Message<\"ito.DeleteNoteRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message ito.DeleteNoteRequest.\n * Use `create(DeleteNoteRequestSchema)` to create a new message.\n */\nexport const DeleteNoteRequestSchema: GenMessage<DeleteNoteRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 13);\n\n/**\n * Interactions\n * -----------------------------------------------------------------\n *\n * @generated from message ito.Interaction\n */\nexport type Interaction = Message<\"ito.Interaction\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * @generated from field: string user_id = 2;\n   */\n  userId: string;\n\n  /**\n   * @generated from field: string title = 3;\n   */\n  title: string;\n\n  /**\n   * JSON string\n   *\n   * @generated from field: string asr_output = 4;\n   */\n  asrOutput: string;\n\n  /**\n   * JSON string\n   *\n   * @generated from field: string llm_output = 5;\n   */\n  llmOutput: string;\n\n  /**\n   * 100 MB limit \n   *\n   * @generated from field: bytes raw_audio = 6;\n   */\n  rawAudio: Uint8Array;\n\n  /**\n   * Duration in milliseconds\n   *\n   * @generated from field: int32 duration_ms = 7;\n   */\n  durationMs: number;\n\n  /**\n   * @generated from field: string created_at = 8;\n   */\n  createdAt: string;\n\n  /**\n   * @generated from field: string updated_at = 9;\n   */\n  updatedAt: string;\n\n  /**\n   * @generated from field: string deleted_at = 10;\n   */\n  deletedAt: string;\n\n  /**\n   * UUID reference to S3 stored audio\n   *\n   * @generated from field: optional string raw_audio_id = 11;\n   */\n  rawAudioId?: string;\n};\n\n/**\n * Describes the message ito.Interaction.\n * Use `create(InteractionSchema)` to create a new message.\n */\nexport const InteractionSchema: GenMessage<Interaction> = /*@__PURE__*/\n  messageDesc(file_ito, 14);\n\n/**\n * @generated from message ito.CreateInteractionRequest\n */\nexport type CreateInteractionRequest = Message<\"ito.CreateInteractionRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * @generated from field: string title = 2;\n   */\n  title: string;\n\n  /**\n   * @generated from field: string asr_output = 3;\n   */\n  asrOutput: string;\n\n  /**\n   * @generated from field: string llm_output = 4;\n   */\n  llmOutput: string;\n\n  /**\n   * 100 MB limit\n   *\n   * @generated from field: bytes raw_audio = 5;\n   */\n  rawAudio: Uint8Array;\n\n  /**\n   * Duration in milliseconds\n   *\n   * @generated from field: int32 duration_ms = 6;\n   */\n  durationMs: number;\n};\n\n/**\n * Describes the message ito.CreateInteractionRequest.\n * Use `create(CreateInteractionRequestSchema)` to create a new message.\n */\nexport const CreateInteractionRequestSchema: GenMessage<CreateInteractionRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 15);\n\n/**\n * @generated from message ito.GetInteractionRequest\n */\nexport type GetInteractionRequest = Message<\"ito.GetInteractionRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message ito.GetInteractionRequest.\n * Use `create(GetInteractionRequestSchema)` to create a new message.\n */\nexport const GetInteractionRequestSchema: GenMessage<GetInteractionRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 16);\n\n/**\n * @generated from message ito.ListInteractionsRequest\n */\nexport type ListInteractionsRequest = Message<\"ito.ListInteractionsRequest\"> & {\n  /**\n   * Optional. ISO 8601 format. If not provided, fetch all.\n   *\n   * @generated from field: string since_timestamp = 1;\n   */\n  sinceTimestamp: string;\n};\n\n/**\n * Describes the message ito.ListInteractionsRequest.\n * Use `create(ListInteractionsRequestSchema)` to create a new message.\n */\nexport const ListInteractionsRequestSchema: GenMessage<ListInteractionsRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 17);\n\n/**\n * @generated from message ito.ListInteractionsResponse\n */\nexport type ListInteractionsResponse = Message<\"ito.ListInteractionsResponse\"> & {\n  /**\n   * @generated from field: repeated ito.Interaction interactions = 1;\n   */\n  interactions: Interaction[];\n};\n\n/**\n * Describes the message ito.ListInteractionsResponse.\n * Use `create(ListInteractionsResponseSchema)` to create a new message.\n */\nexport const ListInteractionsResponseSchema: GenMessage<ListInteractionsResponse> = /*@__PURE__*/\n  messageDesc(file_ito, 18);\n\n/**\n * @generated from message ito.UpdateInteractionRequest\n */\nexport type UpdateInteractionRequest = Message<\"ito.UpdateInteractionRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * @generated from field: string title = 2;\n   */\n  title: string;\n};\n\n/**\n * Describes the message ito.UpdateInteractionRequest.\n * Use `create(UpdateInteractionRequestSchema)` to create a new message.\n */\nexport const UpdateInteractionRequestSchema: GenMessage<UpdateInteractionRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 19);\n\n/**\n * @generated from message ito.DeleteInteractionRequest\n */\nexport type DeleteInteractionRequest = Message<\"ito.DeleteInteractionRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message ito.DeleteInteractionRequest.\n * Use `create(DeleteInteractionRequestSchema)` to create a new message.\n */\nexport const DeleteInteractionRequestSchema: GenMessage<DeleteInteractionRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 20);\n\n/**\n * Dictionary\n * -----------------------------------------------------------------\n *\n * @generated from message ito.DictionaryItem\n */\nexport type DictionaryItem = Message<\"ito.DictionaryItem\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * @generated from field: string user_id = 2;\n   */\n  userId: string;\n\n  /**\n   * @generated from field: string word = 3;\n   */\n  word: string;\n\n  /**\n   * @generated from field: string pronunciation = 4;\n   */\n  pronunciation: string;\n\n  /**\n   * @generated from field: string created_at = 5;\n   */\n  createdAt: string;\n\n  /**\n   * @generated from field: string updated_at = 6;\n   */\n  updatedAt: string;\n\n  /**\n   * @generated from field: string deleted_at = 7;\n   */\n  deletedAt: string;\n};\n\n/**\n * Describes the message ito.DictionaryItem.\n * Use `create(DictionaryItemSchema)` to create a new message.\n */\nexport const DictionaryItemSchema: GenMessage<DictionaryItem> = /*@__PURE__*/\n  messageDesc(file_ito, 21);\n\n/**\n * @generated from message ito.CreateDictionaryItemRequest\n */\nexport type CreateDictionaryItemRequest = Message<\"ito.CreateDictionaryItemRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * @generated from field: string word = 2;\n   */\n  word: string;\n\n  /**\n   * @generated from field: string pronunciation = 3;\n   */\n  pronunciation: string;\n};\n\n/**\n * Describes the message ito.CreateDictionaryItemRequest.\n * Use `create(CreateDictionaryItemRequestSchema)` to create a new message.\n */\nexport const CreateDictionaryItemRequestSchema: GenMessage<CreateDictionaryItemRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 22);\n\n/**\n * @generated from message ito.ListDictionaryItemsRequest\n */\nexport type ListDictionaryItemsRequest = Message<\"ito.ListDictionaryItemsRequest\"> & {\n  /**\n   * Optional. ISO 8601 format. If not provided, fetch all.\n   *\n   * @generated from field: string since_timestamp = 1;\n   */\n  sinceTimestamp: string;\n};\n\n/**\n * Describes the message ito.ListDictionaryItemsRequest.\n * Use `create(ListDictionaryItemsRequestSchema)` to create a new message.\n */\nexport const ListDictionaryItemsRequestSchema: GenMessage<ListDictionaryItemsRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 23);\n\n/**\n * @generated from message ito.ListDictionaryItemsResponse\n */\nexport type ListDictionaryItemsResponse = Message<\"ito.ListDictionaryItemsResponse\"> & {\n  /**\n   * @generated from field: repeated ito.DictionaryItem items = 1;\n   */\n  items: DictionaryItem[];\n};\n\n/**\n * Describes the message ito.ListDictionaryItemsResponse.\n * Use `create(ListDictionaryItemsResponseSchema)` to create a new message.\n */\nexport const ListDictionaryItemsResponseSchema: GenMessage<ListDictionaryItemsResponse> = /*@__PURE__*/\n  messageDesc(file_ito, 24);\n\n/**\n * @generated from message ito.UpdateDictionaryItemRequest\n */\nexport type UpdateDictionaryItemRequest = Message<\"ito.UpdateDictionaryItemRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * @generated from field: string word = 2;\n   */\n  word: string;\n\n  /**\n   * @generated from field: string pronunciation = 3;\n   */\n  pronunciation: string;\n};\n\n/**\n * Describes the message ito.UpdateDictionaryItemRequest.\n * Use `create(UpdateDictionaryItemRequestSchema)` to create a new message.\n */\nexport const UpdateDictionaryItemRequestSchema: GenMessage<UpdateDictionaryItemRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 25);\n\n/**\n * @generated from message ito.DeleteDictionaryItemRequest\n */\nexport type DeleteDictionaryItemRequest = Message<\"ito.DeleteDictionaryItemRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message ito.DeleteDictionaryItemRequest.\n * Use `create(DeleteDictionaryItemRequestSchema)` to create a new message.\n */\nexport const DeleteDictionaryItemRequestSchema: GenMessage<DeleteDictionaryItemRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 26);\n\n/**\n * User Data\n * -----------------------------------------------------------------\n *\n * Empty - user_id will be extracted from the authenticated user's token\n *\n * @generated from message ito.DeleteUserDataRequest\n */\nexport type DeleteUserDataRequest = Message<\"ito.DeleteUserDataRequest\"> & {\n};\n\n/**\n * Describes the message ito.DeleteUserDataRequest.\n * Use `create(DeleteUserDataRequestSchema)` to create a new message.\n */\nexport const DeleteUserDataRequestSchema: GenMessage<DeleteUserDataRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 27);\n\n/**\n * @generated from message ito.LlmSettings\n */\nexport type LlmSettings = Message<\"ito.LlmSettings\"> & {\n  /**\n   * @generated from field: optional string asr_model = 1;\n   */\n  asrModel?: string;\n\n  /**\n   * @generated from field: optional string asr_provider = 2;\n   */\n  asrProvider?: string;\n\n  /**\n   * @generated from field: optional string asr_prompt = 3;\n   */\n  asrPrompt?: string;\n\n  /**\n   * @generated from field: optional string llm_provider = 4;\n   */\n  llmProvider?: string;\n\n  /**\n   * @generated from field: optional string llm_model = 5;\n   */\n  llmModel?: string;\n\n  /**\n   * @generated from field: optional float llm_temperature = 6;\n   */\n  llmTemperature?: number;\n\n  /**\n   * @generated from field: optional string transcription_prompt = 7;\n   */\n  transcriptionPrompt?: string;\n\n  /**\n   * @generated from field: optional string editing_prompt = 8;\n   */\n  editingPrompt?: string;\n\n  /**\n   * @generated from field: optional float no_speech_threshold = 9;\n   */\n  noSpeechThreshold?: number;\n\n  /**\n   * @generated from field: optional float low_quality_threshold = 10;\n   */\n  lowQualityThreshold?: number;\n};\n\n/**\n * Describes the message ito.LlmSettings.\n * Use `create(LlmSettingsSchema)` to create a new message.\n */\nexport const LlmSettingsSchema: GenMessage<LlmSettings> = /*@__PURE__*/\n  messageDesc(file_ito, 28);\n\n/**\n * @generated from message ito.AdvancedSettings\n */\nexport type AdvancedSettings = Message<\"ito.AdvancedSettings\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * @generated from field: string user_id = 2;\n   */\n  userId: string;\n\n  /**\n   * @generated from field: string created_at = 3;\n   */\n  createdAt: string;\n\n  /**\n   * @generated from field: string updated_at = 4;\n   */\n  updatedAt: string;\n\n  /**\n   * @generated from field: ito.LlmSettings llm = 5;\n   */\n  llm?: LlmSettings;\n\n  /**\n   * @generated from field: ito.LlmSettings default = 6;\n   */\n  default?: LlmSettings;\n};\n\n/**\n * Describes the message ito.AdvancedSettings.\n * Use `create(AdvancedSettingsSchema)` to create a new message.\n */\nexport const AdvancedSettingsSchema: GenMessage<AdvancedSettings> = /*@__PURE__*/\n  messageDesc(file_ito, 29);\n\n/**\n * Empty - user_id will be extracted from the authenticated user's token\n *\n * @generated from message ito.GetAdvancedSettingsRequest\n */\nexport type GetAdvancedSettingsRequest = Message<\"ito.GetAdvancedSettingsRequest\"> & {\n};\n\n/**\n * Describes the message ito.GetAdvancedSettingsRequest.\n * Use `create(GetAdvancedSettingsRequestSchema)` to create a new message.\n */\nexport const GetAdvancedSettingsRequestSchema: GenMessage<GetAdvancedSettingsRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 30);\n\n/**\n * @generated from message ito.UpdateAdvancedSettingsRequest\n */\nexport type UpdateAdvancedSettingsRequest = Message<\"ito.UpdateAdvancedSettingsRequest\"> & {\n  /**\n   * @generated from field: ito.LlmSettings llm = 1;\n   */\n  llm?: LlmSettings;\n};\n\n/**\n * Describes the message ito.UpdateAdvancedSettingsRequest.\n * Use `create(UpdateAdvancedSettingsRequestSchema)` to create a new message.\n */\nexport const UpdateAdvancedSettingsRequestSchema: GenMessage<UpdateAdvancedSettingsRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 31);\n\n/**\n * Timing Analytics\n * -----------------------------------------------------------------\n *\n * @generated from message ito.TimingEvent\n */\nexport type TimingEvent = Message<\"ito.TimingEvent\"> & {\n  /**\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * @generated from field: double start_ms = 2;\n   */\n  startMs: number;\n\n  /**\n   * @generated from field: optional double end_ms = 3;\n   */\n  endMs?: number;\n\n  /**\n   * @generated from field: optional double duration_ms = 4;\n   */\n  durationMs?: number;\n};\n\n/**\n * Describes the message ito.TimingEvent.\n * Use `create(TimingEventSchema)` to create a new message.\n */\nexport const TimingEventSchema: GenMessage<TimingEvent> = /*@__PURE__*/\n  messageDesc(file_ito, 32);\n\n/**\n * @generated from message ito.TimingReport\n */\nexport type TimingReport = Message<\"ito.TimingReport\"> & {\n  /**\n   * @generated from field: string interaction_id = 1;\n   */\n  interactionId: string;\n\n  /**\n   * @generated from field: string user_id = 2;\n   */\n  userId: string;\n\n  /**\n   * @generated from field: string platform = 3;\n   */\n  platform: string;\n\n  /**\n   * @generated from field: string app_version = 4;\n   */\n  appVersion: string;\n\n  /**\n   * @generated from field: string hostname = 5;\n   */\n  hostname: string;\n\n  /**\n   * @generated from field: string architecture = 6;\n   */\n  architecture: string;\n\n  /**\n   * @generated from field: string timestamp = 7;\n   */\n  timestamp: string;\n\n  /**\n   * @generated from field: repeated ito.TimingEvent events = 8;\n   */\n  events: TimingEvent[];\n\n  /**\n   * @generated from field: double total_duration_ms = 9;\n   */\n  totalDurationMs: number;\n};\n\n/**\n * Describes the message ito.TimingReport.\n * Use `create(TimingReportSchema)` to create a new message.\n */\nexport const TimingReportSchema: GenMessage<TimingReport> = /*@__PURE__*/\n  messageDesc(file_ito, 33);\n\n/**\n * @generated from message ito.SubmitTimingReportsRequest\n */\nexport type SubmitTimingReportsRequest = Message<\"ito.SubmitTimingReportsRequest\"> & {\n  /**\n   * @generated from field: repeated ito.TimingReport reports = 1;\n   */\n  reports: TimingReport[];\n};\n\n/**\n * Describes the message ito.SubmitTimingReportsRequest.\n * Use `create(SubmitTimingReportsRequestSchema)` to create a new message.\n */\nexport const SubmitTimingReportsRequestSchema: GenMessage<SubmitTimingReportsRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 34);\n\n/**\n * Empty response\n *\n * @generated from message ito.SubmitTimingReportsResponse\n */\nexport type SubmitTimingReportsResponse = Message<\"ito.SubmitTimingReportsResponse\"> & {\n};\n\n/**\n * Describes the message ito.SubmitTimingReportsResponse.\n * Use `create(SubmitTimingReportsResponseSchema)` to create a new message.\n */\nexport const SubmitTimingReportsResponseSchema: GenMessage<SubmitTimingReportsResponse> = /*@__PURE__*/\n  messageDesc(file_ito, 35);\n\n/**\n * @generated from enum ito.ItoMode\n */\nexport enum ItoMode {\n  /**\n   * @generated from enum value: TRANSCRIBE = 0;\n   */\n  TRANSCRIBE = 0,\n\n  /**\n   * @generated from enum value: EDIT = 1;\n   */\n  EDIT = 1,\n}\n\n/**\n * Describes the enum ito.ItoMode.\n */\nexport const ItoModeSchema: GenEnum<ItoMode> = /*@__PURE__*/\n  enumDesc(file_ito, 0);\n\n/**\n * Error Types\n * -----------------------------------------------------------------\n *\n * @generated from enum ito.ClientProvider\n */\nexport enum ClientProvider {\n  /**\n   * @generated from enum value: GROQ = 0;\n   */\n  GROQ = 0,\n\n  /**\n   * @generated from enum value: CEREBRAS = 1;\n   */\n  CEREBRAS = 1,\n}\n\n/**\n * Describes the enum ito.ClientProvider.\n */\nexport const ClientProviderSchema: GenEnum<ClientProvider> = /*@__PURE__*/\n  enumDesc(file_ito, 1);\n\n/**\n * @generated from enum ito.ErrorType\n */\nexport enum ErrorType {\n  /**\n   * @generated from enum value: CONFIGURATION = 0;\n   */\n  CONFIGURATION = 0,\n\n  /**\n   * @generated from enum value: AVAILABILITY = 1;\n   */\n  AVAILABILITY = 1,\n\n  /**\n   * @generated from enum value: AUDIO = 2;\n   */\n  AUDIO = 2,\n\n  /**\n   * @generated from enum value: API = 3;\n   */\n  API = 3,\n}\n\n/**\n * Describes the enum ito.ErrorType.\n */\nexport const ErrorTypeSchema: GenEnum<ErrorType> = /*@__PURE__*/\n  enumDesc(file_ito, 2);\n\n/**\n * @generated from service ito.ItoService\n */\nexport const ItoService: GenService<{\n  /**\n   * Streams audio chunks from the client and gets a single response.\n   * This is the ideal method for dictation to reduce latency and memory usage.\n   *\n   * @generated from rpc ito.ItoService.TranscribeStream\n   */\n  transcribeStream: {\n    methodKind: \"client_streaming\";\n    input: typeof AudioChunkSchema;\n    output: typeof TranscriptionResponseSchema;\n  },\n  /**\n   * Enhanced streaming transcription that accepts configuration data in-stream.\n   * Config can be sent before, during, or omitted entirely. Multiple config messages\n   * are merged by the server. This allows immediate streaming without waiting for context.\n   *\n   * @generated from rpc ito.ItoService.TranscribeStreamV2\n   */\n  transcribeStreamV2: {\n    methodKind: \"client_streaming\";\n    input: typeof TranscribeStreamRequestSchema;\n    output: typeof TranscriptionResponseSchema;\n  },\n  /**\n   * Note Service\n   *\n   * @generated from rpc ito.ItoService.CreateNote\n   */\n  createNote: {\n    methodKind: \"unary\";\n    input: typeof CreateNoteRequestSchema;\n    output: typeof NoteSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.GetNote\n   */\n  getNote: {\n    methodKind: \"unary\";\n    input: typeof GetNoteRequestSchema;\n    output: typeof NoteSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.ListNotes\n   */\n  listNotes: {\n    methodKind: \"unary\";\n    input: typeof ListNotesRequestSchema;\n    output: typeof ListNotesResponseSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.UpdateNote\n   */\n  updateNote: {\n    methodKind: \"unary\";\n    input: typeof UpdateNoteRequestSchema;\n    output: typeof NoteSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.DeleteNote\n   */\n  deleteNote: {\n    methodKind: \"unary\";\n    input: typeof DeleteNoteRequestSchema;\n    output: typeof EmptySchema;\n  },\n  /**\n   * Interaction Service\n   *\n   * @generated from rpc ito.ItoService.CreateInteraction\n   */\n  createInteraction: {\n    methodKind: \"unary\";\n    input: typeof CreateInteractionRequestSchema;\n    output: typeof InteractionSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.GetInteraction\n   */\n  getInteraction: {\n    methodKind: \"unary\";\n    input: typeof GetInteractionRequestSchema;\n    output: typeof InteractionSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.ListInteractions\n   */\n  listInteractions: {\n    methodKind: \"unary\";\n    input: typeof ListInteractionsRequestSchema;\n    output: typeof ListInteractionsResponseSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.UpdateInteraction\n   */\n  updateInteraction: {\n    methodKind: \"unary\";\n    input: typeof UpdateInteractionRequestSchema;\n    output: typeof InteractionSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.DeleteInteraction\n   */\n  deleteInteraction: {\n    methodKind: \"unary\";\n    input: typeof DeleteInteractionRequestSchema;\n    output: typeof EmptySchema;\n  },\n  /**\n   * Dictionary Service\n   *\n   * @generated from rpc ito.ItoService.CreateDictionaryItem\n   */\n  createDictionaryItem: {\n    methodKind: \"unary\";\n    input: typeof CreateDictionaryItemRequestSchema;\n    output: typeof DictionaryItemSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.ListDictionaryItems\n   */\n  listDictionaryItems: {\n    methodKind: \"unary\";\n    input: typeof ListDictionaryItemsRequestSchema;\n    output: typeof ListDictionaryItemsResponseSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.UpdateDictionaryItem\n   */\n  updateDictionaryItem: {\n    methodKind: \"unary\";\n    input: typeof UpdateDictionaryItemRequestSchema;\n    output: typeof DictionaryItemSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.DeleteDictionaryItem\n   */\n  deleteDictionaryItem: {\n    methodKind: \"unary\";\n    input: typeof DeleteDictionaryItemRequestSchema;\n    output: typeof EmptySchema;\n  },\n  /**\n   * User Data Service\n   *\n   * @generated from rpc ito.ItoService.DeleteUserData\n   */\n  deleteUserData: {\n    methodKind: \"unary\";\n    input: typeof DeleteUserDataRequestSchema;\n    output: typeof EmptySchema;\n  },\n  /**\n   * Advanced Settings Service\n   *\n   * @generated from rpc ito.ItoService.GetAdvancedSettings\n   */\n  getAdvancedSettings: {\n    methodKind: \"unary\";\n    input: typeof GetAdvancedSettingsRequestSchema;\n    output: typeof AdvancedSettingsSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.UpdateAdvancedSettings\n   */\n  updateAdvancedSettings: {\n    methodKind: \"unary\";\n    input: typeof UpdateAdvancedSettingsRequestSchema;\n    output: typeof AdvancedSettingsSchema;\n  },\n}> = /*@__PURE__*/\n  serviceDesc(file_ito, 0);\n\n/**\n * @generated from service ito.TimingService\n */\nexport const TimingService: GenService<{\n  /**\n   * Submit timing reports for interaction analytics\n   *\n   * @generated from rpc ito.TimingService.SubmitTimingReports\n   */\n  submitTimingReports: {\n    methodKind: \"unary\";\n    input: typeof SubmitTimingReportsRequestSchema;\n    output: typeof SubmitTimingReportsResponseSchema;\n  },\n}> = /*@__PURE__*/\n  serviceDesc(file_ito, 1);\n\n"
  },
  {
    "path": "app/hooks/useBillingState.test.ts",
    "content": "import { describe, it, expect, mock, beforeEach, afterEach } from 'bun:test'\nimport React from 'react'\nimport { createRoot, Root } from 'react-dom/client'\nimport { act } from 'react'\nimport { Window } from 'happy-dom'\n\nlet window: Window\nlet document: any\nlet mockAddEventListener: ReturnType<typeof mock>\nlet mockRemoveEventListener: ReturnType<typeof mock>\n\nconst mockBillingApi = {\n  status: mock(),\n}\n\nconst mockTrialApi = {\n  complete: mock(),\n}\n\nconst mockApi = {\n  billing: mockBillingApi,\n  trial: mockTrialApi,\n  send: mock(),\n}\n\nconst mockElectronStore = {\n  get: mock((key: string) => {\n    if (key === 'auth') {\n      return mockStoreData.auth\n    }\n    return {}\n  }),\n  set: mock(),\n}\n\nconst mockStoreData: { auth: { billing?: any } } = {\n  auth: {},\n}\n\nconst originalWindow = globalThis.window\n\nbeforeEach(() => {\n  window = new Window()\n  document = window.document\n  global.window = window as any\n  global.document = document as any\n\n  // Reset mock state\n  mockStoreData.auth = {}\n  mockBillingApi.status.mockClear()\n  mockTrialApi.complete.mockClear()\n  mockApi.send.mockClear()\n  mockElectronStore.get.mockClear()\n\n  // Create fresh mocks for event listeners\n  mockAddEventListener = mock((event: string, handler: () => void) => {})\n  mockRemoveEventListener = mock((event: string, handler: () => void) => {})\n\n  // Setup window mocks with addEventListener/removeEventListener\n  globalThis.window = {\n    ...window,\n    addEventListener: mockAddEventListener as any,\n    removeEventListener: mockRemoveEventListener as any,\n    api: mockApi as any,\n    electron: {\n      store: mockElectronStore as any,\n    },\n  } as any\n})\n\nafterEach(() => {\n  globalThis.window = originalWindow\n})\n\n// Simple test utility to render a hook\nfunction renderHook<T>(hook: () => T): {\n  result: { current: T }\n  rerender: () => void\n  unmount: () => void\n  waitFor: (fn: () => boolean, timeout?: number) => Promise<void>\n} {\n  const result: { current: T } = { current: null as any }\n  let root: Root | null = null\n  let container: any = null\n\n  const TestComponent = () => {\n    const hookResult = hook()\n    result.current = hookResult\n    return null\n  }\n\n  const mount = () => {\n    container = document.createElement('div')\n    root = createRoot(container)\n\n    act(() => {\n      root!.render(React.createElement(TestComponent))\n    })\n\n    // Wait for initial render to complete\n    return new Promise<void>(resolve => {\n      setTimeout(() => resolve(), 0)\n    })\n  }\n\n  // Mount synchronously wrapped in act\n  mount()\n\n  const rerender = () => {\n    if (root && container) {\n      act(() => {\n        root!.render(React.createElement(TestComponent))\n      })\n    }\n  }\n\n  const unmount = () => {\n    if (root) {\n      act(() => {\n        root?.unmount()\n      })\n      root = null\n    }\n    if (container && container.parentNode) {\n      container.parentNode.removeChild(container)\n    }\n    container = null\n  }\n\n  const waitFor = async (fn: () => boolean, timeout = 1000): Promise<void> => {\n    const start = Date.now()\n    while (Date.now() - start < timeout) {\n      // Ensure result.current is not null before checking\n      if (result.current !== null && fn()) {\n        return\n      }\n      await new Promise(resolve => setTimeout(resolve, 10))\n    }\n    throw new Error('waitFor timeout')\n  }\n\n  return { result, rerender, unmount, waitFor }\n}\n\nimport { useBillingState } from './useBillingState'\n\ndescribe('useBillingState', () => {\n  it('initializes with loading state and no cached data', async () => {\n    mockBillingApi.status.mockResolvedValue({\n      success: true,\n      pro_status: 'none' as const,\n      trial: {\n        trialDays: 14,\n        trialStartAt: null,\n        daysLeft: 0,\n        isTrialActive: false,\n        hasCompletedTrial: false,\n      },\n    })\n\n    const { result, waitFor } = renderHook(() => useBillingState())\n\n    await waitFor(() => !result.current.isLoading)\n\n    expect(result.current.isLoading).toBe(false)\n    expect(result.current.error).toBe(null)\n    expect(result.current.proStatus).toBe('none')\n    expect(result.current.isPro).toBe(false)\n    expect(mockBillingApi.status).toHaveBeenCalledTimes(1)\n  })\n\n  it('loads cached billing state from electron store', async () => {\n    const cachedState = {\n      proStatus: 'free_trial' as const,\n      subscriptionStartAt: '2024-01-01T00:00:00.000Z',\n      trialDays: 14,\n      trialStartAt: '2024-01-01T00:00:00.000Z',\n      daysLeft: 10,\n      isTrialActive: true,\n      hasCompletedTrial: false,\n    }\n\n    mockStoreData.auth.billing = cachedState\n\n    // Mock API response to match cached state (refresh() will be called after mount)\n    mockBillingApi.status.mockResolvedValue({\n      success: true,\n      pro_status: 'free_trial' as const,\n      trial: {\n        trialDays: 14,\n        trialStartAt: '2024-01-01T00:00:00.000Z',\n        daysLeft: 10,\n        isTrialActive: true,\n        hasCompletedTrial: false,\n      },\n    })\n\n    const { result, waitFor } = renderHook(() => useBillingState())\n\n    await waitFor(() => !result.current.isLoading)\n\n    expect(result.current.proStatus).toBe('free_trial')\n    expect(result.current.isTrialActive).toBe(true)\n    expect(result.current.daysLeft).toBe(10)\n  })\n\n  it('handles successful billing status fetch', async () => {\n    const mockResponse = {\n      success: true,\n      pro_status: 'active_pro' as const,\n      subscriptionStartAt: '2024-01-01T00:00:00.000Z',\n      trial: {\n        trialDays: 14,\n        trialStartAt: null,\n        daysLeft: 0,\n        isTrialActive: false,\n        hasCompletedTrial: true,\n      },\n    }\n\n    mockBillingApi.status.mockResolvedValue(mockResponse)\n\n    const { result, waitFor } = renderHook(() => useBillingState())\n\n    await waitFor(() => !result.current.isLoading)\n\n    expect(result.current.proStatus).toBe('active_pro')\n    expect(result.current.isPro).toBe(true)\n    expect(result.current.hasSubscription).toBe(true)\n    expect(result.current.error).toBe(null)\n    expect(result.current.subscriptionStartAt).toBeInstanceOf(Date)\n    expect(mockApi.send).toHaveBeenCalledWith(\n      'electron-store-set',\n      'auth.billing',\n      expect.objectContaining({\n        proStatus: 'active_pro',\n      }),\n    )\n  })\n\n  it('handles billing status fetch error', async () => {\n    mockBillingApi.status.mockResolvedValue({\n      success: false,\n      error: 'API error',\n    })\n\n    const { result, waitFor } = renderHook(() => useBillingState())\n\n    await waitFor(() => !result.current.isLoading)\n\n    expect(result.current.error).toBe('API error')\n    expect(result.current.proStatus).toBe('none')\n  })\n\n  it('handles billing status fetch exception', async () => {\n    const error = new Error('Network error')\n    mockBillingApi.status.mockRejectedValue(error)\n\n    const { result, waitFor } = renderHook(() => useBillingState())\n\n    await waitFor(() => !result.current.isLoading)\n\n    expect(result.current.error).toBe('Network error')\n    expect(result.current.proStatus).toBe('none')\n  })\n\n  it('refresh function updates billing state', async () => {\n    const initialResponse = {\n      success: true,\n      pro_status: 'none' as const,\n      trial: {\n        trialDays: 14,\n        trialStartAt: null,\n        daysLeft: 0,\n        isTrialActive: false,\n        hasCompletedTrial: false,\n      },\n    }\n\n    const updatedResponse = {\n      success: true,\n      pro_status: 'active_pro' as const,\n      subscriptionStartAt: '2024-01-01T00:00:00.000Z',\n      trial: {\n        trialDays: 14,\n        trialStartAt: null,\n        daysLeft: 0,\n        isTrialActive: false,\n        hasCompletedTrial: true,\n      },\n    }\n\n    mockBillingApi.status.mockResolvedValueOnce(initialResponse)\n    mockBillingApi.status.mockResolvedValueOnce(updatedResponse)\n\n    const { result, waitFor } = renderHook(() => useBillingState())\n\n    await waitFor(() => !result.current.isLoading)\n\n    expect(result.current.proStatus).toBe('none')\n\n    await result.current.refresh()\n\n    await waitFor(() => result.current.proStatus === 'active_pro')\n\n    expect(result.current.proStatus).toBe('active_pro')\n    expect(result.current.isPro).toBe(true)\n    expect(mockBillingApi.status).toHaveBeenCalledTimes(2)\n  })\n\n  it('completeTrial function calls trial.complete and refreshes', async () => {\n    const billingResponse = {\n      success: true,\n      pro_status: 'none' as const,\n      trial: {\n        trialDays: 14,\n        trialStartAt: '2024-01-01T00:00:00.000Z',\n        daysLeft: 5,\n        isTrialActive: true,\n        hasCompletedTrial: false,\n      },\n    }\n\n    const trialCompleteResponse = {\n      success: true,\n      trialDays: 14,\n      trialStartAt: '2024-01-01T00:00:00.000Z',\n      daysLeft: 5,\n      isTrialActive: false,\n      hasCompletedTrial: true,\n    }\n\n    mockBillingApi.status.mockResolvedValue(billingResponse)\n    mockTrialApi.complete.mockResolvedValue(trialCompleteResponse)\n\n    const { result, waitFor } = renderHook(() => useBillingState())\n\n    await waitFor(() => !result.current.isLoading)\n\n    mockBillingApi.status.mockClear()\n\n    await result.current.completeTrial()\n\n    await waitFor(() => !result.current.isLoading)\n\n    expect(mockTrialApi.complete).toHaveBeenCalledTimes(1)\n    expect(mockBillingApi.status).toHaveBeenCalledTimes(1)\n  })\n\n  it('completeTrial handles errors gracefully', async () => {\n    const billingResponse = {\n      success: true,\n      pro_status: 'none' as const,\n      trial: {\n        trialDays: 14,\n        trialStartAt: null,\n        daysLeft: 0,\n        isTrialActive: false,\n        hasCompletedTrial: false,\n      },\n    }\n\n    mockBillingApi.status.mockResolvedValue(billingResponse)\n    mockTrialApi.complete.mockResolvedValue({\n      success: false,\n      error: 'Trial completion failed',\n    })\n\n    const { result, waitFor } = renderHook(() => useBillingState())\n\n    await waitFor(() => !result.current.isLoading)\n\n    // Clear the mock so refresh() won't be called after completeTrial\n    mockBillingApi.status.mockClear()\n\n    await result.current.completeTrial()\n\n    await waitFor(\n      () => !result.current.isLoading && result.current.error !== null,\n    )\n\n    expect(result.current.error).toBe('Trial completion failed')\n  })\n\n  it('handles missing trial data gracefully', async () => {\n    const mockResponse = {\n      success: true,\n      pro_status: 'none' as const,\n      trial: undefined,\n    }\n\n    mockBillingApi.status.mockResolvedValue(mockResponse)\n\n    const { result, waitFor } = renderHook(() => useBillingState())\n\n    await waitFor(() => !result.current.isLoading)\n\n    expect(result.current.proStatus).toBe('none')\n    expect(result.current.trialDays).toBe(14)\n    expect(result.current.daysLeft).toBe(0)\n    expect(result.current.isTrialActive).toBe(false)\n  })\n\n  it('converts date strings to Date objects correctly', async () => {\n    const mockResponse = {\n      success: true,\n      pro_status: 'active_pro' as const,\n      subscriptionStartAt: '2024-01-15T10:30:00.000Z',\n      trial: {\n        trialDays: 14,\n        trialStartAt: '2024-01-01T00:00:00.000Z',\n        daysLeft: 5,\n        isTrialActive: false,\n        hasCompletedTrial: true,\n      },\n    }\n\n    mockBillingApi.status.mockResolvedValue(mockResponse)\n\n    const { result, waitFor } = renderHook(() => useBillingState())\n\n    await waitFor(() => !result.current.isLoading)\n\n    expect(result.current.subscriptionStartAt).toBeInstanceOf(Date)\n    expect(result.current.subscriptionStartAt?.toISOString()).toBe(\n      '2024-01-15T10:30:00.000Z',\n    )\n    expect(result.current.trialStartAt).toBeInstanceOf(Date)\n    expect(result.current.trialStartAt?.toISOString()).toBe(\n      '2024-01-01T00:00:00.000Z',\n    )\n  })\n\n  it('handles cache read errors gracefully', async () => {\n    mockElectronStore.get.mockImplementation(() => {\n      throw new Error('Store read error')\n    })\n\n    mockBillingApi.status.mockResolvedValue({\n      success: true,\n      pro_status: 'none' as const,\n      trial: {\n        trialDays: 14,\n        trialStartAt: null,\n        daysLeft: 0,\n        isTrialActive: false,\n        hasCompletedTrial: false,\n      },\n    })\n\n    const { result, waitFor } = renderHook(() => useBillingState())\n\n    await waitFor(() => !result.current.isLoading)\n\n    expect(result.current.proStatus).toBe('none')\n    expect(mockBillingApi.status).toHaveBeenCalledTimes(1)\n  })\n\n  it('handles cache write errors gracefully', async () => {\n    mockApi.send.mockImplementation(() => {\n      throw new Error('Store write error')\n    })\n\n    const mockResponse = {\n      success: true,\n      pro_status: 'active_pro' as const,\n      subscriptionStartAt: '2024-01-01T00:00:00.000Z',\n      trial: {\n        trialDays: 14,\n        trialStartAt: null,\n        daysLeft: 0,\n        isTrialActive: false,\n        hasCompletedTrial: true,\n      },\n    }\n\n    mockBillingApi.status.mockResolvedValue(mockResponse)\n\n    const { result, waitFor } = renderHook(() => useBillingState())\n\n    await waitFor(() => !result.current.isLoading)\n\n    expect(result.current.proStatus).toBe('active_pro')\n    expect(result.current.error).toBe(null)\n  })\n\n  it('sets up window focus listener on mount', async () => {\n    mockBillingApi.status.mockResolvedValue({\n      success: true,\n      pro_status: 'none' as const,\n      trial: {\n        trialDays: 14,\n        trialStartAt: null,\n        daysLeft: 0,\n        isTrialActive: false,\n        hasCompletedTrial: false,\n      },\n    })\n\n    const { result, waitFor, unmount } = renderHook(() => useBillingState())\n\n    await waitFor(() => !result.current.isLoading)\n\n    expect(mockAddEventListener).toHaveBeenCalledWith(\n      'focus',\n      expect.any(Function),\n    )\n\n    unmount()\n\n    expect(mockRemoveEventListener).toHaveBeenCalledWith(\n      'focus',\n      expect.any(Function),\n    )\n  })\n\n  it('refreshes billing state when window gains focus', async () => {\n    mockBillingApi.status.mockResolvedValue({\n      success: true,\n      pro_status: 'none' as const,\n      trial: {\n        trialDays: 14,\n        trialStartAt: null,\n        daysLeft: 0,\n        isTrialActive: false,\n        hasCompletedTrial: false,\n      },\n    })\n\n    const { result, waitFor } = renderHook(() => useBillingState())\n\n    await waitFor(() => !result.current.isLoading)\n\n    mockBillingApi.status.mockClear()\n\n    const focusHandler = mockAddEventListener.mock.calls.find(\n      call => call[0] === 'focus',\n    )?.[1] as () => void\n\n    expect(focusHandler).toBeDefined()\n\n    if (focusHandler) {\n      await focusHandler()\n      await waitFor(() => mockBillingApi.status.mock.calls.length > 0)\n      expect(mockBillingApi.status).toHaveBeenCalledTimes(1)\n    }\n  })\n\n  it('sets up periodic refresh interval', async () => {\n    const originalSetInterval = global.setInterval\n    const mockSetInterval = mock(() => ({}) as any)\n    global.setInterval = mockSetInterval as any\n\n    mockBillingApi.status.mockResolvedValue({\n      success: true,\n      pro_status: 'none' as const,\n      trial: {\n        trialDays: 14,\n        trialStartAt: null,\n        daysLeft: 0,\n        isTrialActive: false,\n        hasCompletedTrial: false,\n      },\n    })\n\n    const { result, waitFor, unmount } = renderHook(() => useBillingState())\n\n    await waitFor(() => !result.current.isLoading)\n\n    expect(mockSetInterval).toHaveBeenCalledWith(\n      expect.any(Function),\n      2 * 60 * 1000,\n    )\n\n    unmount()\n\n    global.setInterval = originalSetInterval\n  })\n\n  it('periodic refresh calls refresh function', async () => {\n    const originalSetInterval = global.setInterval\n    const originalClearInterval = global.clearInterval\n    let intervalCallback: (() => void) | null = null\n    const intervalId: any = {}\n    const mockSetInterval = mock((callback: () => void, delay: number) => {\n      intervalCallback = callback\n      return intervalId\n    })\n    const mockClearInterval = mock(() => {})\n    global.setInterval = mockSetInterval as any\n    global.clearInterval = mockClearInterval as any\n\n    mockBillingApi.status.mockResolvedValue({\n      success: true,\n      pro_status: 'none' as const,\n      trial: {\n        trialDays: 14,\n        trialStartAt: null,\n        daysLeft: 0,\n        isTrialActive: false,\n        hasCompletedTrial: false,\n      },\n    })\n\n    const { result, waitFor, unmount } = renderHook(() => useBillingState())\n\n    await waitFor(() => !result.current.isLoading)\n\n    mockBillingApi.status.mockClear()\n\n    if (intervalCallback) {\n      await (intervalCallback as () => void)()\n      await waitFor(() => mockBillingApi.status.mock.calls.length > 0)\n      expect(mockBillingApi.status).toHaveBeenCalledTimes(1)\n    }\n\n    unmount()\n\n    expect(mockClearInterval).toHaveBeenCalledWith(intervalId)\n\n    global.setInterval = originalSetInterval\n    global.clearInterval = originalClearInterval\n  })\n})\n"
  },
  {
    "path": "app/hooks/useBillingState.ts",
    "content": "import { useCallback, useEffect, useMemo, useState } from 'react'\n\nexport type BillingState = {\n  proStatus: 'active_pro' | 'free_trial' | 'none'\n  subscriptionStartAt: Date | null\n  subscriptionEndAt: Date | null\n  isScheduledForCancellation: boolean\n  trialDays: number\n  trialStartAt: Date | null\n  daysLeft: number\n  isTrialActive: boolean\n  hasCompletedTrial: boolean\n}\n\nexport function useBillingState() {\n  const [isLoading, setIsLoading] = useState<boolean>(true)\n  const [error, setError] = useState<string | null>(null)\n  const [state, setState] = useState<BillingState | null>(null)\n\n  useEffect(() => {\n    try {\n      const authStore = window.electron?.store?.get('auth') || {}\n      const cached: (BillingState & { fetchedAt?: string }) | undefined =\n        authStore?.billing\n      if (cached) {\n        setState({\n          ...cached,\n          subscriptionStartAt: cached.subscriptionStartAt\n            ? new Date(cached.subscriptionStartAt)\n            : null,\n          subscriptionEndAt: cached.subscriptionEndAt\n            ? new Date(cached.subscriptionEndAt)\n            : null,\n          trialStartAt: cached.trialStartAt\n            ? new Date(cached.trialStartAt)\n            : null,\n        })\n      }\n    } catch {\n      console.warn('Failed to load billing state from cache')\n    } finally {\n      setIsLoading(false)\n    }\n  }, [])\n\n  const cacheState = useCallback((s: BillingState) => {\n    try {\n      const withFetchedAt = { ...s, fetchedAt: new Date().toISOString() }\n      window.api.send('electron-store-set', 'auth.billing', withFetchedAt)\n    } catch {\n      console.warn('Failed to cache billing state')\n    }\n  }, [])\n\n  const refresh = useCallback(async () => {\n    setIsLoading(true)\n    setError(null)\n    try {\n      const res = await window.api.billing.status()\n      if (!res?.success) {\n        setError(res?.error || 'Failed to load billing status')\n      } else {\n        const subStart = res?.subscriptionStartAt\n          ? new Date(res.subscriptionStartAt)\n          : null\n        const subEnd = res?.subscriptionEndAt\n          ? new Date(res.subscriptionEndAt)\n          : null\n        const trial = res?.trial || {}\n        const next: BillingState = {\n          proStatus: res.pro_status,\n          subscriptionStartAt: subStart,\n          subscriptionEndAt: subEnd,\n          isScheduledForCancellation: !!res?.isScheduledForCancellation,\n          trialDays: trial.trialDays ?? 14,\n          trialStartAt: trial.trialStartAt\n            ? new Date(trial.trialStartAt)\n            : null,\n          daysLeft: trial.daysLeft ?? 0,\n          isTrialActive: !!trial.isTrialActive,\n          hasCompletedTrial: !!trial.hasCompletedTrial,\n        }\n        setState(next)\n        cacheState(next)\n      }\n    } catch (e: any) {\n      setError(e?.message || 'Failed to load billing status')\n    } finally {\n      setIsLoading(false)\n    }\n  }, [cacheState])\n\n  useEffect(() => {\n    refresh()\n  }, [refresh])\n\n  // Periodically refresh billing state to stay in sync with webhook updates\n  useEffect(() => {\n    const interval = setInterval(\n      () => {\n        refresh()\n      },\n      2 * 60 * 1000,\n    ) // Refresh every 2 minutes\n\n    return () => clearInterval(interval)\n  }, [refresh])\n\n  // Refresh billing state when window regains focus\n  useEffect(() => {\n    const handleFocus = () => {\n      refresh()\n    }\n\n    window.addEventListener('focus', handleFocus)\n\n    return () => {\n      window.removeEventListener('focus', handleFocus)\n    }\n  }, [refresh])\n\n  const completeTrial = useCallback(async () => {\n    setIsLoading(true)\n    setError(null)\n    try {\n      const res = await window.api.trial.complete()\n      if (!res?.success) {\n        setError(res?.error || 'Failed to complete trial')\n      } else {\n        await refresh()\n      }\n    } catch (e: any) {\n      setError(e?.message || 'Failed to complete trial')\n    } finally {\n      setIsLoading(false)\n    }\n  }, [refresh])\n\n  const api = useMemo(\n    () => ({\n      isLoading,\n      error,\n      proStatus: state?.proStatus ?? 'none',\n      isPro: (state?.proStatus ?? 'none') === 'active_pro',\n      hasSubscription: (state?.proStatus ?? 'none') === 'active_pro',\n      subscriptionStartAt: state?.subscriptionStartAt ?? null,\n      subscriptionEndAt: state?.subscriptionEndAt ?? null,\n      isScheduledForCancellation: state?.isScheduledForCancellation ?? false,\n\n      isTrialActive: !!state?.isTrialActive,\n      daysLeft: state?.daysLeft ?? 0,\n      trialDays: state?.trialDays ?? 14,\n      trialStartAt: state?.trialStartAt ?? null,\n      hasCompletedTrial: !!state?.hasCompletedTrial,\n      refresh,\n      completeTrial,\n    }),\n    [isLoading, error, state, refresh, completeTrial],\n  )\n\n  return api\n}\n\nexport default useBillingState\n"
  },
  {
    "path": "app/hooks/useDeviceChangeListener.ts",
    "content": "import { useEffect } from 'react'\nimport log from 'electron-log'\n\n/**\n * A React hook that listens for changes in media devices (e.g., plugging in or\n * unplugging a microphone/headset) and notifies the main process.\n * This should be used once in a long-lived component, like the root App component.\n */\nexport const useDeviceChangeListener = (): void => {\n  useEffect(() => {\n    // Define the handler function that will be called on the event.\n    const handleDeviceChange = () => {\n      console.log(\n        '[Renderer] `devicechange` event detected. Notifying main process.',\n      )\n      window.api.send('audio-devices-changed')\n    }\n\n    // Add the event listener when the component mounts.\n    navigator.mediaDevices.addEventListener('devicechange', handleDeviceChange)\n\n    // Return a cleanup function to remove the listener when the component unmounts.\n    // This is crucial for preventing memory leaks and ensuring good practice.\n    return () => {\n      navigator.mediaDevices.removeEventListener(\n        'devicechange',\n        handleDeviceChange,\n      )\n      console.log('[useDeviceChangeListener] Removed devicechange listener.')\n    }\n  }, []) // The empty dependency array ensures this effect runs only once on mount.\n}\n"
  },
  {
    "path": "app/hooks/usePlatform.ts",
    "content": "import { useState, useEffect } from 'react'\n\ntype Platform = 'darwin' | 'win32'\n\nexport function usePlatform(): Platform | undefined {\n  const [platform, setPlatform] = useState<Platform | undefined>(undefined)\n\n  useEffect(() => {\n    window.api.getPlatform().then(setPlatform)\n  }, [])\n\n  return platform\n}\n"
  },
  {
    "path": "app/index.d.ts",
    "content": "/// <reference types=\"electron-vite/node\" />\n\ndeclare module '*.css' {\n  const content: string\n  export default content\n}\n\ndeclare module '*.png' {\n  const content: string\n  export default content\n}\n\ndeclare module '*.jpg' {\n  const content: string\n  export default content\n}\n\ndeclare module '*.jpeg' {\n  const content: string\n  export default content\n}\n\ndeclare module '*.svg' {\n  const content: string\n  export default content\n}\n\ndeclare module '*.webm' {\n  const content: string\n  export default content\n}\n\ndeclare module '*.web' {\n  const content: string\n  export default content\n}\n\n// Augment the Window interface\ndeclare global {\n  interface Window {\n    api: IpcApi\n  }\n}\n\nexport interface IpcApi {\n  generateNewAuthState: () => Promise<any>\n  invoke: (channel: string, ...args: any[]) => Promise<any>\n  on: (\n    channel: string,\n    listener: (event: any, ...args: any[]) => void,\n  ) => () => void // Returns a cleanup function\n  send: (channel: string, ...args: any[]) => void\n  getNativeAudioDevices: () => Promise<any>\n  notifyLoginSuccess: (\n    profile: any,\n    idToken: string,\n    accessToken: string,\n  ) => void\n  deleteUserData: () => Promise<void>\n}\n"
  },
  {
    "path": "app/index.html",
    "content": "<!doctype html>\n<html lang=\"en\" class=\"light\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Ito</title>\n  </head>\n\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/renderer.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "app/media/microphone.ts",
    "content": "import { useSettingsStore } from '../store/useSettingsStore'\n\ntype Microphone = {\n  deviceId: string\n  label: string\n}\n\ntype MicrophoneToRender = {\n  title: string\n  description?: string\n}\n\nasync function getAvailableMicrophones(): Promise<Microphone[]> {\n  try {\n    console.log('Fetching available native microphones...')\n    // This now gets the list directly from our Rust binary via the main process\n    const deviceNames: string[] = await window.api.invoke(\n      'get-native-audio-devices',\n    )\n    console.log('Available native microphones:', deviceNames)\n    // The deviceId and label are the same in this new system\n    return deviceNames.map(name => ({\n      deviceId: name,\n      label: name,\n    }))\n  } catch (error) {\n    console.error('Error getting available native microphones:', error)\n    return []\n  }\n}\n\n/**\n * Verifies if the currently selected microphone in settings is still connected.\n * If not, it gracefully falls back to the \"default\" auto-detect setting.\n */\nexport async function verifyStoredMicrophone() {\n  try {\n    console.log('[verifyStoredMicrophone] Verifying selected microphone...')\n    const { microphoneDeviceId, setMicrophoneDeviceId } =\n      useSettingsStore.getState()\n\n    // If the user already has \"default\" selected, there's nothing to verify.\n    if (microphoneDeviceId === 'default') {\n      console.log(\n        '[verifyStoredMicrophone] \"Auto-detect\" is selected. Verification not needed.',\n      )\n      return\n    }\n\n    // Get the list of currently available microphones from the native backend.\n    const availableDevices: string[] = await window.api.invoke(\n      'get-native-audio-devices',\n    )\n\n    // Check if the stored deviceId is in the list of available devices.\n    const isDeviceAvailable = availableDevices.includes(microphoneDeviceId)\n\n    if (isDeviceAvailable) {\n      console.log(\n        `[verifyStoredMicrophone] Stored microphone \"${microphoneDeviceId}\" is still available.`,\n      )\n    } else {\n      console.warn(\n        `[verifyStoredMicrophone] Stored microphone \"${microphoneDeviceId}\" is not available. Falling back to \"Auto-detect\".`,\n      )\n      // The device is disconnected. Update the store to use the default.\n      // We pass the friendly name \"Auto-detect\" to keep the UI consistent.\n      setMicrophoneDeviceId('default', 'Auto-detect')\n    }\n  } catch (error) {\n    console.error(\n      '[verifyStoredMicrophone] Failed to verify microphone:',\n      error,\n    )\n  }\n}\n\nconst microphoneToRender = (microphone: Microphone): MicrophoneToRender => {\n  const label = microphone.label.toLowerCase()\n\n  // Handle default device case\n  if (label.includes('default -')) {\n    return {\n      title: `Auto-detect`,\n      description:\n        'May connect to Bluetooth earbuds, slowing transcription speed',\n    }\n  }\n\n  // Handle built-in microphone\n  if (label.includes('built-in') || label.includes('macbook pro microphone')) {\n    return {\n      title: 'Built-in mic (recommended)',\n    }\n  }\n\n  // Default case - return original label\n  return {\n    title: microphone.label,\n  }\n}\n\nexport { getAvailableMicrophones, microphoneToRender }\n\nexport type { Microphone, MicrophoneToRender }\n"
  },
  {
    "path": "app/renderer.tsx",
    "content": "import './sentry'\nimport React from 'react'\nimport ReactDOM from 'react-dom/client'\nimport App from './app'\n\nif (window.location.hash !== '#/pill') {\n  import('@/app/styles/app.css')\n}\n\nReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n)\n"
  },
  {
    "path": "app/sentry.ts",
    "content": "import * as Sentry from '@sentry/electron/renderer'\n\nconst dsn = import.meta.env.VITE_SENTRY_DSN as string | undefined\nconst environment =\n  (import.meta.env.VITE_SENTRY_ENV as string | undefined) || 'local'\n\nconst tracesSampleRate = Number.parseFloat(\n  (import.meta.env.VITE_SENTRY_TRACES_SAMPLE_RATE as string | undefined) ||\n    '0.2',\n)\n\nconst profilesSampleRate = Number.parseFloat(\n  (import.meta.env.VITE_SENTRY_PROFILES_SAMPLE_RATE as string | undefined) ||\n    '0.2',\n)\n\nSentry.init({\n  enabled: Boolean(dsn),\n  dsn,\n  environment,\n  tracesSampleRate,\n  profilesSampleRate,\n  beforeBreadcrumb: breadcrumb =>\n    breadcrumb?.category === 'console' ? null : breadcrumb,\n  integrations: integrations =>\n    integrations.filter(integration =>\n      typeof (integration as any).name === 'string'\n        ? !(\n            (integration as any).name.toLowerCase().includes('console') ||\n            (integration as any).name.toLowerCase() === 'breadcrumbs'\n          )\n        : true,\n    ),\n})\n"
  },
  {
    "path": "app/store/useAdvancedSettingsStore.ts",
    "content": "import { create } from 'zustand'\nimport { STORE_KEYS } from '../../lib/constants/store-keys'\n\nexport interface LlmSettings {\n  asrProvider: string | null\n  asrModel: string | null\n  asrPrompt: string | null\n  llmProvider: string | null\n  llmModel: string | null\n  llmTemperature: number | null\n  transcriptionPrompt: string | null\n  editingPrompt: string | null\n  noSpeechThreshold: number | null\n}\n\ninterface AdvancedSettingsState {\n  llm: LlmSettings\n  grammarServiceEnabled: boolean\n  defaults?: LlmSettings\n  macosAccessibilityContextEnabled: boolean\n  setLlmSettings: (settings: Partial<LlmSettings>) => void\n  setGrammarServiceEnabled: (enabled: boolean) => void\n  setMacosAccessibilityContextEnabled: (enabled: boolean) => void\n}\n\n// Initialize from electron store\nconst getInitialState = () => {\n  const storedAdvancedSettings = window.electron.store.get(\n    STORE_KEYS.ADVANCED_SETTINGS,\n  )\n\n  return {\n    llm: storedAdvancedSettings.llm,\n    grammarServiceEnabled:\n      storedAdvancedSettings.grammarServiceEnabled ?? false,\n    defaults: storedAdvancedSettings.defaults,\n    macosAccessibilityContextEnabled:\n      storedAdvancedSettings.macosAccessibilityContextEnabled ?? false,\n  }\n}\n\n// Sync to electron store\nconst syncToStore = (state: Partial<AdvancedSettingsState>) => {\n  const currentAdvancedSettings =\n    window.electron.store.get(STORE_KEYS.ADVANCED_SETTINGS) || {}\n\n  const updatedAdvancedSettings = {\n    ...currentAdvancedSettings,\n    ...state,\n  }\n\n  window.electron.store.set(\n    STORE_KEYS.ADVANCED_SETTINGS,\n    updatedAdvancedSettings,\n  )\n}\n\nexport const useAdvancedSettingsStore = create<AdvancedSettingsState>(set => {\n  const initialState = getInitialState()\n\n  // Subscribe to updates from sync service\n  const handleStoreUpdate = () => {\n    const latestState = getInitialState()\n    set(latestState)\n  }\n\n  window.api.on('advanced-settings-updated', handleStoreUpdate)\n\n  return {\n    ...initialState,\n    setLlmSettings: (settings: Partial<LlmSettings>) => {\n      set(state => {\n        const newLlmSettings = { ...state.llm, ...settings }\n        const partialState = { llm: newLlmSettings }\n        syncToStore(partialState)\n        return partialState\n      })\n    },\n    setGrammarServiceEnabled: (enabled: boolean) => {\n      set(() => {\n        const partialState = { grammarServiceEnabled: enabled }\n        syncToStore(partialState)\n        return partialState\n      })\n    },\n    setMacosAccessibilityContextEnabled: (enabled: boolean) => {\n      set(() => {\n        const partialState = { macosAccessibilityContextEnabled: enabled }\n        syncToStore(partialState)\n        return partialState\n      })\n    },\n  }\n})\n"
  },
  {
    "path": "app/store/useAudioStore.ts",
    "content": "import { create } from 'zustand'\nimport log from 'electron-log'\n\ninterface AudioState {\n  isRecording: boolean\n  isShortcutEnabled: boolean\n  setIsShortcutEnabled: (enabled: boolean) => void\n  startRecording: () => Promise<void>\n  stopRecording: () => Promise<void>\n}\n\nexport const useAudioStore = create<AudioState>((set, get) => ({\n  isRecording: false,\n  isShortcutEnabled: true,\n\n  setIsShortcutEnabled: (enabled: boolean) => {\n    set({ isShortcutEnabled: enabled })\n  },\n\n  startRecording: async () => {\n    const { isRecording, isShortcutEnabled } = get()\n    if (isRecording || !isShortcutEnabled) return\n\n    console.log('[AudioStore] Starting native recording...')\n    set({ isRecording: true })\n    // Signal the main process to start the gRPC stream and tell the\n    // native recorder to begin capturing.\n    window.api.send('start-native-recording')\n  },\n\n  stopRecording: async () => {\n    const { isRecording } = get()\n    if (!isRecording) return\n\n    console.log('[AudioStore] Stopping native recording...')\n    // Signal the main process to tell the native recorder to stop\n    // and to close the gRPC stream.\n    window.api.send('stop-native-recording')\n    set({ isRecording: false })\n  },\n}))\n"
  },
  {
    "path": "app/store/useAuthStore.ts",
    "content": "import { create } from 'zustand'\nimport type {\n  AuthState,\n  AuthUser,\n  AuthTokens,\n  AuthStore,\n} from '../../lib/main/store'\nimport { STORE_KEYS } from '../../lib/constants/store-keys'\n\ninterface AuthZustandStore {\n  // State\n  isAuthenticated: boolean\n  user: AuthUser | null\n  tokens: AuthTokens | null\n  state: AuthState | null\n  isLoading: boolean\n  error: string | null\n  isSelfHosted: boolean\n\n  // Actions\n  setAuthData: (tokens: AuthTokens, user: AuthUser, provider?: string) => void\n  clearAuth: (preserveUser?: boolean) => void\n  setLoading: (loading: boolean) => void\n  setError: (error: string | null) => void\n  updateUser: (user: Partial<AuthUser>) => void\n  updateState: (state: Partial<AuthState>) => void\n  setName: (name: string) => void\n  setSelfHostedMode: () => void\n}\n\n// Initialize from electron store\nconst getInitialState = () => {\n  const storedAuth = window.electron?.store?.get(STORE_KEYS.AUTH) as\n    | (AuthStore & { isSelfHosted?: boolean })\n    | undefined\n\n  // Generate new auth state if no stored auth stat\n\n  return {\n    isAuthenticated:\n      !!storedAuth?.tokens?.access_token || !!storedAuth?.isSelfHosted,\n    user: storedAuth?.user || null,\n    tokens: storedAuth?.tokens || null,\n    state: storedAuth?.state || null,\n    isLoading: false,\n    error: null,\n    isSelfHosted: !!storedAuth?.isSelfHosted,\n  }\n}\n\n// Sync to electron store\nconst syncToStore = (state: {\n  user?: AuthUser | null\n  tokens?: AuthTokens | null\n  state?: AuthState | null\n  isSelfHosted?: boolean\n}) => {\n  if (!window.electron?.store) return\n\n  const currentStore = window.electron.store.get(STORE_KEYS.AUTH) || {}\n  const updates: any = { ...currentStore }\n\n  if ('user' in state) {\n    updates.user = state.user\n  }\n\n  if ('tokens' in state) {\n    updates.tokens = state.tokens\n  }\n\n  if ('state' in state) {\n    updates.state = state.state\n  }\n\n  if ('isSelfHosted' in state) {\n    updates.isSelfHosted = state.isSelfHosted\n  }\n\n  window.electron.store.set(STORE_KEYS.AUTH, updates)\n}\n\nexport const useAuthStore = create<AuthZustandStore>((set, get) => {\n  const initialState = getInitialState()\n\n  return {\n    ...initialState,\n\n    setAuthData: (tokens: AuthTokens, user: AuthUser, provider?: string) => {\n      // Calculate expires_at if not provided\n      const expiresAt =\n        tokens.expires_at ||\n        (tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : undefined)\n\n      const enhancedUser: AuthUser = {\n        ...user,\n        provider,\n        lastSignInAt: new Date().toISOString(),\n      }\n\n      const enhancedTokens = {\n        ...tokens,\n        expires_at: expiresAt,\n      }\n\n      const newState = {\n        isAuthenticated: true,\n        tokens: enhancedTokens,\n        user: enhancedUser,\n        state: get().state || null,\n        error: null,\n      }\n\n      syncToStore({ tokens: enhancedTokens, user: enhancedUser })\n      set(newState)\n    },\n\n    clearAuth: (preserveUser: boolean = true) => {\n      const currentUser = get().user\n\n      const newState = {\n        isAuthenticated: false,\n        user: preserveUser ? currentUser : null,\n        tokens: null,\n        state: null,\n        error: null,\n        isSelfHosted: false,\n      }\n\n      syncToStore({\n        tokens: null,\n        user: preserveUser ? currentUser : null,\n        state: null,\n        isSelfHosted: false,\n      })\n      set(newState)\n    },\n\n    setLoading: (loading: boolean) => {\n      set({ isLoading: loading })\n    },\n\n    setError: (error: string | null) => {\n      set({ error })\n    },\n\n    updateUser: (userUpdate: Partial<AuthUser>) => {\n      const currentUser = get().user\n      if (!currentUser) return\n\n      const updatedUser = { ...currentUser, ...userUpdate }\n      syncToStore({ user: updatedUser })\n      set({ user: updatedUser })\n    },\n\n    setName: (name: string) => {\n      const currentUser = get().user\n      if (!currentUser) return\n\n      const updatedUser = { ...currentUser, name }\n      syncToStore({ user: updatedUser })\n      set({ user: updatedUser })\n    },\n\n    updateState: (stateUpdate: Partial<AuthState>) => {\n      const currentState = get().state\n      if (!currentState) return\n\n      const updatedState = { ...currentState, ...stateUpdate }\n      syncToStore({ state: updatedState })\n      set({ state: updatedState })\n    },\n\n    setSelfHostedMode: () => {\n      const selfHostedUser: AuthUser = {\n        id: 'self-hosted',\n        provider: 'self-hosted',\n        lastSignInAt: new Date().toISOString(),\n      }\n\n      const newState = {\n        isAuthenticated: true,\n        isSelfHosted: true,\n        user: selfHostedUser,\n        tokens: null, // No tokens needed for self-hosted\n        error: null,\n      }\n\n      syncToStore({ user: selfHostedUser, isSelfHosted: true })\n      set(newState)\n    },\n  }\n})\n"
  },
  {
    "path": "app/store/useDictionaryStore.ts",
    "content": "import { create } from 'zustand'\nimport { useAuthStore } from './useAuthStore'\nimport type { DictionaryItem } from '../../lib/main/sqlite/models'\n\nexport type DictionaryEntry = {\n  id: string\n  type: 'normal' | 'replacement'\n  createdAt: string // Changed to string to match DB\n  updatedAt: string // Changed to string to match DB\n} & (\n  | {\n      type: 'normal'\n      content: string\n    }\n  | {\n      type: 'replacement'\n      from: string\n      to: string\n    }\n)\n\ninterface DictionaryStore {\n  entries: DictionaryEntry[]\n  loadEntries: () => Promise<void>\n  addEntry: (content: string) => Promise<void>\n  addReplacement: (from: string, to: string) => Promise<void>\n  updateEntry: (\n    id: string,\n    updates: Partial<Omit<DictionaryEntry, 'id' | 'createdAt' | 'type'>>,\n  ) => Promise<void>\n  deleteEntry: (id: string) => Promise<void>\n}\n\n/**\n * The backend stores a flat DictionaryItem, but the frontend uses a more\n * structured DictionaryEntry. This function maps the backend type to the\n * frontend type.\n * We infer the type based on whether `pronunciation` is null.\n */\nconst mapItemToEntry = (item: DictionaryItem): DictionaryEntry => {\n  if (item.pronunciation === null || item.pronunciation === '') {\n    return {\n      id: item.id,\n      type: 'normal',\n      content: item.word,\n      createdAt: item.created_at,\n      updatedAt: item.updated_at,\n    }\n  } else {\n    return {\n      id: item.id,\n      type: 'replacement',\n      from: item.word,\n      to: item.pronunciation,\n      createdAt: item.created_at,\n      updatedAt: item.updated_at,\n    }\n  }\n}\n\nexport const useDictionaryStore = create<DictionaryStore>((set, get) => ({\n  entries: [],\n\n  loadEntries: async () => {\n    try {\n      const items = await window.api.dictionary.getAll()\n      const entries = items.map(mapItemToEntry)\n      set({ entries })\n    } catch (error) {\n      console.error('Failed to load dictionary from database:', error)\n    }\n  },\n\n  addEntry: async (content: string) => {\n    const { user } = useAuthStore.getState()\n    if (!user) return\n    const result = await window.api.dictionary.add({\n      user_id: user.id,\n      word: content.trim(),\n      pronunciation: null,\n    })\n    if (!result.success) {\n      throw new Error(result.error)\n    }\n    const newEntry = mapItemToEntry(result.data)\n    set(state => ({ entries: [newEntry, ...state.entries] }))\n  },\n\n  addReplacement: async (from: string, to: string) => {\n    const { user } = useAuthStore.getState()\n    if (!user) return\n    const result = await window.api.dictionary.add({\n      user_id: user.id,\n      word: from.trim(),\n      pronunciation: to.trim(),\n    })\n    if (!result.success) {\n      throw new Error(result.error)\n    }\n    const newEntry = mapItemToEntry(result.data)\n    set(state => ({ entries: [newEntry, ...state.entries] }))\n  },\n\n  updateEntry: async (id, updates) => {\n    const originalEntry = get().entries.find(e => e.id === id)\n    if (!originalEntry) return\n\n    // Create a new entry object with the updates applied\n    const updatedEntry = { ...originalEntry, ...updates }\n\n    let word: string\n    let pronunciation: string | null\n\n    if (updatedEntry.type === 'normal') {\n      word = updatedEntry.content\n      pronunciation = null\n    } else {\n      word = updatedEntry.from\n      pronunciation = updatedEntry.to\n    }\n\n    const result = await window.api.dictionary.update(id, word, pronunciation)\n    if (!result.success) {\n      throw new Error(result.error)\n    }\n    get().loadEntries() // Reload all entries to reflect the change\n  },\n\n  deleteEntry: async (id: string) => {\n    try {\n      await window.api.dictionary.delete(id)\n      set(state => ({ entries: state.entries.filter(e => e.id !== id) }))\n    } catch (error) {\n      console.error('Failed to delete dictionary entry:', error)\n    }\n  },\n}))\n"
  },
  {
    "path": "app/store/useMainStore.ts",
    "content": "import { create } from 'zustand'\nimport { STORE_KEYS } from '../../lib/constants/store-keys'\n\ntype PageType = 'home' | 'dictionary' | 'notes' | 'settings' | 'about'\ntype SettingsPageType =\n  | 'general'\n  | 'keyboard'\n  | 'audio'\n  | 'account'\n  | 'advanced'\n  | 'pricing-billing'\n\ninterface MainStore {\n  navExpanded: boolean\n  currentPage: PageType\n  settingsPage: SettingsPageType\n  toggleNavExpanded: () => void\n  setCurrentPage: (page: PageType) => void\n  setSettingsPage: (page: SettingsPageType) => void\n}\n\n// Initialize from electron store\nconst getInitialState = () => {\n  const storedMain = window.electron.store.get(STORE_KEYS.MAIN)\n\n  return {\n    navExpanded: storedMain?.navExpanded ?? true,\n    currentPage: (storedMain?.currentPage as PageType) ?? 'home',\n    settingsPage: (storedMain?.settingsPage as SettingsPageType) ?? 'general',\n  }\n}\n\n// Sync to electron store\nconst syncToStore = (state: Partial<MainStore>) => {\n  const currentStore = window.electron.store.get(STORE_KEYS.MAIN) || {}\n  const updates: any = { ...currentStore }\n\n  if ('navExpanded' in state) {\n    updates.navExpanded = state.navExpanded ?? currentStore.navExpanded\n  }\n\n  if ('settingsPage' in state) {\n    updates.settingsPage = state.settingsPage ?? currentStore.settingsPage\n  }\n\n  window.electron.store.set(STORE_KEYS.MAIN, updates)\n}\n\nexport const useMainStore = create<MainStore>(set => {\n  const initialState = getInitialState()\n  return {\n    navExpanded: initialState.navExpanded,\n    currentPage: 'home',\n    settingsPage: initialState.settingsPage,\n    toggleNavExpanded: () =>\n      set(state => {\n        const newState = { navExpanded: !state.navExpanded }\n        syncToStore(newState)\n        return newState\n      }),\n    setCurrentPage: (page: PageType) => set({ currentPage: page }),\n    setSettingsPage: (page: SettingsPageType) => {\n      const newState = { settingsPage: page }\n      syncToStore(newState)\n      set(newState)\n    },\n  }\n})\n"
  },
  {
    "path": "app/store/useNotesStore.ts",
    "content": "import { create } from 'zustand'\nimport { useAuthStore } from './useAuthStore'\n\nexport type Note = {\n  id: string\n  content: string\n  user_id: string\n  interaction_id: string | null\n  created_at: string\n  updated_at: string\n}\n\ninterface NotesStore {\n  notes: Note[]\n  loadNotes: () => Promise<void>\n  addNote: (content: string) => Promise<void>\n  updateNote: (id: string, content: string) => Promise<void>\n  deleteNote: (id: string) => Promise<void>\n}\n\nexport const useNotesStore = create<NotesStore>((set, get) => ({\n  notes: [],\n\n  loadNotes: async () => {\n    try {\n      const notes = await window.api.notes.getAll()\n      set({ notes })\n    } catch (error) {\n      console.error('Failed to load notes from database:', error)\n    }\n  },\n\n  addNote: async (content: string) => {\n    const { user } = useAuthStore.getState()\n    if (!user) {\n      console.error('Cannot add a note without a logged-in user.')\n      return\n    }\n    try {\n      const newNote = await window.api.notes.add({\n        content: content.trim(),\n        user_id: user.id,\n      })\n      set(state => ({ notes: [newNote, ...state.notes] }))\n    } catch (error) {\n      console.error('Failed to add note to database:', error)\n    }\n  },\n\n  updateNote: async (id: string, content: string) => {\n    try {\n      await window.api.notes.updateContent(id, content)\n      // For an immediate UI update, we can call loadNotes again\n      // or manually update the state.\n      get().loadNotes()\n    } catch (error) {\n      console.error('Failed to update note in database:', error)\n    }\n  },\n\n  deleteNote: async (id: string) => {\n    try {\n      await window.api.notes.delete(id)\n      set(state => ({\n        notes: state.notes.filter(note => note.id !== id),\n      }))\n    } catch (error) {\n      console.error('Failed to delete note from database:', error)\n    }\n  },\n}))\n"
  },
  {
    "path": "app/store/useOnboardingStore.ts",
    "content": "import { create } from 'zustand'\nimport { analytics, ANALYTICS_EVENTS } from '../components/analytics'\nimport { STORE_KEYS } from '../../lib/constants/store-keys'\n\n// Onboarding category constants\nexport const ONBOARDING_CATEGORIES = {\n  SIGN_UP: 'sign-up',\n  PERMISSIONS: 'permissions',\n  SET_UP: 'set-up',\n  TRY_IT: 'try-it',\n} as const\n\n// Export the type so it can be used in other files\nexport type OnboardingCategory =\n  (typeof ONBOARDING_CATEGORIES)[keyof typeof ONBOARDING_CATEGORIES]\n\ninterface OnboardingState {\n  onboardingStep: number\n  totalOnboardingSteps: number\n  onboardingCompleted: boolean\n  onboardingCategory: OnboardingCategory\n  referralSource: string | null\n  incrementOnboardingStep: () => void\n  decrementOnboardingStep: () => void\n  setReferralSource: (source: string) => void\n  setOnboardingCompleted: () => void\n  resetOnboarding: () => void\n  initializeOnboarding: () => void\n}\n\n// Step name constants\nexport const STEP_NAMES = {\n  CREATE_ACCOUNT: 'create_account',\n  REFERRAL_SOURCE: 'referral_source',\n  DATA_CONTROL: 'data_control',\n  PERMISSIONS: 'permissions',\n  MICROPHONE_TEST: 'microphone_test',\n  KEYBOARD_TEST: 'keyboard_test',\n  GOOD_TO_GO: 'good_to_go',\n  INTRODUCING_INTELLIGENT_MODE: 'introducing_intelligent_mode',\n  ANY_APP: 'any_app',\n  TRY_IT_OUT: 'try_it_out',\n}\n\n// Order here matters for onboarding flow\nexport const STEP_NAMES_ARRAY = [\n  STEP_NAMES.CREATE_ACCOUNT,\n  STEP_NAMES.REFERRAL_SOURCE,\n  STEP_NAMES.DATA_CONTROL,\n  STEP_NAMES.PERMISSIONS,\n  STEP_NAMES.MICROPHONE_TEST,\n  STEP_NAMES.KEYBOARD_TEST,\n  STEP_NAMES.GOOD_TO_GO,\n  STEP_NAMES.INTRODUCING_INTELLIGENT_MODE,\n  STEP_NAMES.ANY_APP,\n  STEP_NAMES.TRY_IT_OUT,\n]\n\nconst getOnboardingCategory = (onboardingStep: number): OnboardingCategory => {\n  if (onboardingStep < 3) return ONBOARDING_CATEGORIES.SIGN_UP\n  if (onboardingStep < 4) return ONBOARDING_CATEGORIES.PERMISSIONS\n  if (onboardingStep < 7) return ONBOARDING_CATEGORIES.SET_UP\n  return ONBOARDING_CATEGORIES.TRY_IT\n}\n\nexport const getOnboardingCategoryIndex = (\n  onboardingCategory: OnboardingCategory,\n): number => {\n  if (onboardingCategory === ONBOARDING_CATEGORIES.SIGN_UP) return 0\n  if (onboardingCategory === ONBOARDING_CATEGORIES.PERMISSIONS) return 1\n  if (onboardingCategory === ONBOARDING_CATEGORIES.SET_UP) return 2\n  return 3\n}\n\nconst getStepName = (step: number): string => {\n  return STEP_NAMES_ARRAY[step] || 'unknown'\n}\n\n// Initialize from electron store\nconst getInitialState = () => {\n  const storedOnboarding = window.electron.store.get(STORE_KEYS.ONBOARDING)\n\n  return {\n    onboardingStep: storedOnboarding?.onboardingStep ?? 0,\n    onboardingCompleted: storedOnboarding?.onboardingCompleted ?? false,\n  }\n}\n\n// Sync to electron store\nconst syncToStore = (state: Partial<OnboardingState>) => {\n  if ('onboardingStep' in state || 'onboardingCompleted' in state) {\n    const currentStore = window.electron.store.get(STORE_KEYS.ONBOARDING) || {}\n    window.electron.store.set(STORE_KEYS.ONBOARDING, {\n      ...currentStore,\n      onboardingStep: state.onboardingStep ?? currentStore.onboardingStep,\n      onboardingCompleted:\n        state.onboardingCompleted ?? currentStore.onboardingCompleted,\n    })\n\n    window.api.notifyOnboardingUpdate(state)\n  }\n}\n\nexport const useOnboardingStore = create<OnboardingState>(set => {\n  const initialState = getInitialState()\n  const totalOnboardingSteps = STEP_NAMES_ARRAY.length\n\n  return {\n    onboardingStep: initialState.onboardingStep,\n    totalOnboardingSteps,\n    onboardingCompleted: initialState.onboardingCompleted,\n    onboardingCategory: getOnboardingCategory(initialState.onboardingStep),\n    referralSource: null,\n    incrementOnboardingStep: () =>\n      set(state => {\n        const onboardingStep = Math.min(\n          state.onboardingStep + 1,\n          state.totalOnboardingSteps,\n        )\n        const onboardingCategory = getOnboardingCategory(onboardingStep)\n        const newState = {\n          onboardingStep,\n          onboardingCategory,\n        }\n        // Track onboarding step completion\n        analytics.trackOnboarding(ANALYTICS_EVENTS.ONBOARDING_STEP_COMPLETED, {\n          step: state.onboardingStep, // The step that was just completed\n          step_name: getStepName(state.onboardingStep),\n          category: state.onboardingCategory,\n          total_steps: state.totalOnboardingSteps,\n          referral_source: state.referralSource || undefined,\n        })\n\n        // Track viewing of new step\n        if (onboardingStep < state.totalOnboardingSteps) {\n          analytics.trackOnboarding(ANALYTICS_EVENTS.ONBOARDING_STEP_VIEWED, {\n            step: onboardingStep,\n            step_name: getStepName(onboardingStep),\n            category: onboardingCategory,\n            total_steps: state.totalOnboardingSteps,\n            referral_source: state.referralSource || undefined,\n          })\n        }\n\n        syncToStore(newState)\n        return newState\n      }),\n    decrementOnboardingStep: () =>\n      set(state => {\n        const onboardingStep = Math.max(state.onboardingStep - 1, 0)\n        const onboardingCategory = getOnboardingCategory(onboardingStep)\n        const newState = {\n          onboardingStep,\n          onboardingCategory,\n        }\n        // Track viewing of previous step\n        analytics.trackOnboarding(ANALYTICS_EVENTS.ONBOARDING_STEP_VIEWED, {\n          step: onboardingStep,\n          step_name: getStepName(onboardingStep),\n          category: onboardingCategory,\n          total_steps: state.totalOnboardingSteps,\n          referral_source: state.referralSource || undefined,\n        })\n\n        syncToStore(newState)\n        return newState\n      }),\n    setOnboardingCompleted: () =>\n      set(state => {\n        const step = state.totalOnboardingSteps - 1\n        analytics.trackOnboarding(ANALYTICS_EVENTS.ONBOARDING_STEP_COMPLETED, {\n          step: state.totalOnboardingSteps,\n          step_name: getStepName(step),\n          category: getOnboardingCategory(step),\n          total_steps: state.totalOnboardingSteps,\n        })\n\n        analytics.trackOnboarding(ANALYTICS_EVENTS.ONBOARDING_COMPLETED, {\n          step: state.totalOnboardingSteps,\n          step_name: 'completed',\n          category: ONBOARDING_CATEGORIES.TRY_IT,\n          total_steps: state.totalOnboardingSteps,\n        })\n\n        // Update user properties to mark onboarding as completed\n        analytics.updateUserProperties({\n          onboarding_completed: true,\n          referral_source: state.referralSource || undefined,\n        })\n\n        const newState = { onboardingCompleted: true }\n        syncToStore(newState)\n        return newState\n      }),\n    resetOnboarding: () =>\n      set(_state => {\n        const newState = { onboardingStep: 0, onboardingCompleted: false }\n        analytics.updateUserProperties({\n          onboarding_completed: false,\n        })\n        syncToStore(newState)\n        return newState\n      }),\n    setReferralSource: (source: string) =>\n      set(_state => {\n        const newState = { referralSource: source }\n        analytics.updateUserProperties({\n          referral_source: source,\n        })\n        syncToStore(newState)\n        return newState\n      }),\n    initializeOnboarding: () => {\n      const step = 0\n      const onboardingCategory = getOnboardingCategory(step)\n      analytics.trackOnboarding(ANALYTICS_EVENTS.ONBOARDING_STEP_VIEWED, {\n        step,\n        step_name: getStepName(0),\n        category: onboardingCategory,\n        total_steps: totalOnboardingSteps,\n      })\n    },\n  }\n})\n"
  },
  {
    "path": "app/store/usePermissionsStore.ts",
    "content": "import { create } from 'zustand'\n\ninterface PermissionsState {\n  isAccessibilityEnabled: boolean\n  isMicrophoneEnabled: boolean\n  setAccessibilityEnabled: (enabled: boolean) => void\n  setMicrophoneEnabled: (enabled: boolean) => void\n}\n\nexport const usePermissionsStore = create<PermissionsState>(set => ({\n  isAccessibilityEnabled: false,\n  isMicrophoneEnabled: false,\n  setAccessibilityEnabled: enabled => set({ isAccessibilityEnabled: enabled }),\n  setMicrophoneEnabled: enabled => set({ isMicrophoneEnabled: enabled }),\n}))\n"
  },
  {
    "path": "app/store/useSettingsStore.ts",
    "content": "import { create } from 'zustand'\nimport {\n  analytics,\n  ANALYTICS_EVENTS,\n  updateAnalyticsFromSettings,\n} from '@/app/components/analytics'\nimport { STORE_KEYS } from '../../lib/constants/store-keys'\nimport type { KeyboardShortcutConfig } from '@/lib/main/store'\nimport { ItoMode } from '../generated/ito_pb'\n\nimport { ITO_MODE_SHORTCUT_DEFAULTS } from '@/lib/constants/keyboard-defaults'\nimport {\n  normalizeChord,\n  ShortcutResult,\n  validateShortcutForDuplicate,\n  isReservedCombination,\n} from '../utils/keyboard'\nimport { KeyName } from '@/lib/types/keyboard'\n\ninterface SettingsState {\n  shareAnalytics: boolean\n  launchAtLogin: boolean\n  showItoBarAlways: boolean\n  showAppInDock: boolean\n  interactionSounds: boolean\n  muteAudioWhenDictating: boolean\n  microphoneDeviceId: string\n  microphoneName: string\n  keyboardShortcuts: KeyboardShortcutConfig[]\n  setShareAnalytics: (share: boolean) => void\n  setLaunchAtLogin: (launch: boolean) => void\n  setShowItoBarAlways: (show: boolean) => void\n  setShowAppInDock: (show: boolean) => void\n  setInteractionSounds: (enabled: boolean) => void\n  setMuteAudioWhenDictating: (enabled: boolean) => void\n  setMicrophoneDeviceId: (deviceId: string, name: string) => void\n  createKeyboardShortcut: (mode: ItoMode) => ShortcutResult\n  removeKeyboardShortcut: (shortcutId: string) => void\n  getItoModeShortcuts: (mode: ItoMode) => KeyboardShortcutConfig[]\n  updateKeyboardShortcut: (\n    shortcutId: string,\n    keys: KeyName[],\n  ) => Promise<ShortcutResult>\n}\n\ntype SettingCategory = 'general' | 'audio&mic' | 'keyboard' | 'account'\n\n// Initialize from electron store\nconst getInitialState = () => {\n  const storedSettings = window.electron.store.get(STORE_KEYS.SETTINGS)\n\n  return {\n    shareAnalytics: storedSettings?.shareAnalytics ?? true,\n    launchAtLogin: storedSettings?.launchAtLogin ?? true,\n    showItoBarAlways: storedSettings?.showItoBarAlways ?? true,\n    showAppInDock: storedSettings?.showAppInDock ?? true,\n    interactionSounds: storedSettings?.interactionSounds ?? false,\n    muteAudioWhenDictating: storedSettings?.muteAudioWhenDictating ?? false,\n    microphoneDeviceId: storedSettings?.microphoneDeviceId ?? 'default',\n    microphoneName: storedSettings?.microphoneName ?? 'Default Microphone',\n    keyboardShortcuts: storedSettings?.keyboardShortcuts ?? [\n      {\n        keys: ITO_MODE_SHORTCUT_DEFAULTS[ItoMode.EDIT],\n        mode: ItoMode.EDIT,\n        id: crypto.randomUUID(),\n      },\n      {\n        keys: ITO_MODE_SHORTCUT_DEFAULTS[ItoMode.TRANSCRIBE],\n        mode: ItoMode.TRANSCRIBE,\n        id: crypto.randomUUID(),\n      },\n    ],\n    firstName: storedSettings?.firstName ?? '',\n    lastName: storedSettings?.lastName ?? '',\n    email: storedSettings?.email ?? '',\n  }\n}\n\n// --- START: CORRECTED CODE ---\n\n// Sync to electron store\nconst syncToStore = (state: Partial<SettingsState>) => {\n  const currentSettings = window.electron.store.get(STORE_KEYS.SETTINGS) || {}\n\n  // A much simpler and more robust way to merge the settings.\n  // This takes all existing settings and overwrites them with only the keys\n  // present in the new partial state, without accidentally unsetting others.\n  const updatedSettings = {\n    ...currentSettings,\n    ...state,\n  }\n\n  window.electron.store.set(STORE_KEYS.SETTINGS, updatedSettings)\n\n  // Notify pill window of settings changes\n  if (window.api?.notifySettingsUpdate) {\n    window.api.notifySettingsUpdate(updatedSettings)\n  }\n\n  // Re-register hotkeys when keyboard shortcuts change\n  if ('keyboardShortcuts' in state && window.api?.registerHotkeys) {\n    window.api.registerHotkeys()\n  }\n}\n\nexport const useSettingsStore = create<SettingsState>(set => {\n  const initialState = getInitialState()\n\n  // Helper for single-property setters\n  const createSetter =\n    <K extends keyof SettingsState>(\n      key: K,\n      settingCategory: SettingCategory = 'general',\n    ) =>\n    (value: SettingsState[K]) => {\n      const currentValue = useSettingsStore.getState()[key]\n      const partialState = { [key]: value } as Partial<SettingsState>\n      analytics.trackSettings(ANALYTICS_EVENTS.SETTING_CHANGED, {\n        setting_name: key as string,\n        old_value: currentValue,\n        new_value: value,\n        setting_category: settingCategory,\n      })\n      set(partialState)\n      syncToStore(partialState)\n    }\n\n  return {\n    ...initialState,\n    setShareAnalytics: (share: boolean) => {\n      const partialState = { shareAnalytics: share }\n      set(partialState)\n      syncToStore(partialState)\n      // Update analytics when setting changes\n      updateAnalyticsFromSettings(share)\n    },\n    setLaunchAtLogin: (launch: boolean) => {\n      const currentValue = useSettingsStore.getState().launchAtLogin\n      const partialState = { launchAtLogin: launch }\n      analytics.trackSettings(ANALYTICS_EVENTS.SETTING_CHANGED, {\n        setting_name: 'launchAtLogin',\n        old_value: currentValue,\n        new_value: launch,\n        setting_category: 'general',\n      })\n      set(partialState)\n      syncToStore(partialState)\n      if (window.api?.loginItem?.setSettings) {\n        window.api.loginItem.setSettings(launch)\n      }\n    },\n    setShowItoBarAlways: createSetter('showItoBarAlways', 'general'),\n    setShowAppInDock: (show: boolean) => {\n      const currentValue = useSettingsStore.getState().showAppInDock\n      const partialState = { showAppInDock: show }\n      // Track setting change\n      analytics.trackSettings(ANALYTICS_EVENTS.SETTING_CHANGED, {\n        setting_name: 'showAppInDock',\n        old_value: currentValue,\n        new_value: show,\n        setting_category: 'ui',\n      })\n\n      set(partialState)\n      syncToStore(partialState)\n      if (window.api?.dock?.setVisibility) {\n        window.api.dock.setVisibility(show)\n      }\n    },\n    setInteractionSounds: createSetter('interactionSounds', 'audio&mic'),\n    setMuteAudioWhenDictating: createSetter(\n      'muteAudioWhenDictating',\n      'audio&mic',\n    ),\n    setMicrophoneDeviceId: (deviceId: string, name: string) => {\n      const currentName = useSettingsStore.getState().microphoneName\n      analytics.trackSettings(ANALYTICS_EVENTS.MICROPHONE_CHANGED, {\n        setting_name: 'microphoneName',\n        old_value: currentName,\n        new_value: name,\n        setting_category: 'audio&mic',\n      })\n      const partialState = {\n        microphoneDeviceId: deviceId,\n        microphoneName: name,\n      }\n      set(partialState)\n      syncToStore(partialState)\n    },\n    createKeyboardShortcut: (mode: ItoMode): ShortcutResult => {\n      const currentShortcuts = useSettingsStore.getState().keyboardShortcuts\n\n      const newShortcut = {\n        keys: [],\n        mode,\n        id: crypto.randomUUID(),\n      }\n\n      const newShortcuts = [...currentShortcuts, newShortcut]\n      const partialState = {\n        keyboardShortcuts: newShortcuts,\n      }\n      // Track keyboard shortcut change\n      analytics.trackSettings(ANALYTICS_EVENTS.KEYBOARD_SHORTCUTS_CHANGED, {\n        setting_name: 'keyboardShortcuts',\n        old_value: currentShortcuts,\n        new_value: newShortcuts,\n        setting_category: 'input',\n      })\n\n      // Update user properties\n      analytics.updateUserProperties({\n        keyboard_shortcuts: newShortcuts.map(ks => JSON.stringify(ks)),\n      })\n      set(partialState)\n      syncToStore(partialState)\n      return { success: true }\n    },\n    removeKeyboardShortcut: (shortcutId: string) => {\n      const currentShortcuts = useSettingsStore.getState().keyboardShortcuts\n      const newShortcuts = currentShortcuts.filter(ks => ks.id !== shortcutId)\n      const partialState = {\n        keyboardShortcuts: newShortcuts,\n      }\n      // Track keyboard shortcut change\n      analytics.trackSettings(ANALYTICS_EVENTS.KEYBOARD_SHORTCUTS_CHANGED, {\n        setting_name: 'keyboardShortcuts',\n        old_value: currentShortcuts,\n        new_value: newShortcuts,\n        setting_category: 'input',\n      })\n\n      // Update user properties\n      analytics.updateUserProperties({\n        keyboard_shortcuts: newShortcuts.map(ks => JSON.stringify(ks)),\n      })\n      set(partialState)\n      syncToStore(partialState)\n    },\n    getItoModeShortcuts: (mode: ItoMode) => {\n      const { keyboardShortcuts } = useSettingsStore.getState()\n      return keyboardShortcuts.filter(ks => ks.mode === mode)\n    },\n    updateKeyboardShortcut: async (\n      shortcutId: string,\n      keys: KeyName[],\n    ): Promise<ShortcutResult> => {\n      const currentShortcuts = useSettingsStore.getState()\n        .keyboardShortcuts as KeyboardShortcutConfig[]\n\n      const shortcut = currentShortcuts.find(ks => ks.id === shortcutId)\n\n      if (!shortcut) {\n        return { success: false, error: 'not-found' }\n      }\n\n      const normalizedKeys = normalizeChord(keys)\n\n      // Get platform for validation\n      const platform = await window.api.getPlatform()\n\n      // Check for reserved combinations\n      const reservedCheck = isReservedCombination(normalizedKeys, platform)\n      if (reservedCheck.isReserved) {\n        return {\n          success: false,\n          error: 'reserved-combination',\n          errorMessage: reservedCheck.reason,\n        }\n      }\n\n      const newShortcut = {\n        ...shortcut,\n        keys: normalizedKeys,\n      }\n\n      const duplicateError = validateShortcutForDuplicate(\n        currentShortcuts,\n        newShortcut,\n        shortcut.mode,\n      )\n      if (duplicateError) {\n        return duplicateError\n      }\n\n      const updatedShortcuts = currentShortcuts.map(ks =>\n        ks.id === shortcutId ? { ...ks, keys: normalizedKeys } : ks,\n      )\n      const partialState = {\n        keyboardShortcuts: updatedShortcuts,\n      }\n      // Track keyboard shortcut change\n      analytics.trackSettings(ANALYTICS_EVENTS.KEYBOARD_SHORTCUTS_CHANGED, {\n        setting_name: 'keyboardShortcuts',\n        old_value: currentShortcuts,\n        new_value: updatedShortcuts,\n        setting_category: 'input',\n      })\n\n      // Update user properties\n      analytics.updateUserProperties({\n        keyboard_shortcuts: updatedShortcuts.map(ks => JSON.stringify(ks)),\n      })\n      set(partialState)\n      syncToStore(partialState)\n\n      return { success: true }\n    },\n  }\n})\n\nif (typeof window !== 'undefined' && window.api?.loginItem?.getSettings) {\n  window.api.loginItem\n    .getSettings()\n    .then(settings => {\n      const storedSettings = window.electron.store.get(STORE_KEYS.SETTINGS)\n      if (settings.openAtLogin !== storedSettings?.launchAtLogin) {\n        useSettingsStore.getState().setLaunchAtLogin(settings.openAtLogin)\n      }\n    })\n    .catch(error => {\n      console.error(\n        'Failed to sync login item settings on initialization:',\n        error,\n      )\n    })\n}\n\nif (typeof window !== 'undefined' && window.api?.dock?.getVisibility) {\n  window.api.invoke('init-window').then((windowInfo: any) => {\n    if (windowInfo.platform === 'darwin') {\n      window.api.dock\n        .getVisibility()\n        .then(dockSettings => {\n          const storedSettings = window.electron.store.get(STORE_KEYS.SETTINGS)\n          if (dockSettings.isVisible !== storedSettings?.showAppInDock) {\n            useSettingsStore.getState().setShowAppInDock(dockSettings.isVisible)\n          }\n        })\n        .catch(error => {\n          console.error(\n            'Failed to sync dock visibility on initialization:',\n            error,\n          )\n        })\n    }\n  })\n}\n"
  },
  {
    "path": "app/store/useShortcutEditingStore.ts",
    "content": "import { create } from 'zustand'\n\ninterface ShortcutEditingState {\n  activeEditor: string | null\n  start: (editorKey: string) => boolean\n  stop: (editorKey: string) => void\n  isActive: (editorKey: string) => boolean\n}\n\nexport const useShortcutEditingStore = create<ShortcutEditingState>(\n  (set, get) => ({\n    activeEditor: null,\n    start: (editorKey: string): boolean => {\n      const current = get().activeEditor\n      if (current && current !== editorKey) return false\n      if (current === editorKey) return true\n      set({ activeEditor: editorKey })\n      return true\n    },\n    stop: (editorKey: string): void => {\n      const current = get().activeEditor\n      if (current === editorKey) {\n        set({ activeEditor: null })\n      }\n    },\n    isActive: (editorKey: string): boolean => get().activeEditor === editorKey,\n  }),\n)\n"
  },
  {
    "path": "app/store/useUserMetadataStore.ts",
    "content": "import { create } from 'zustand'\nimport { useAuthStore } from './useAuthStore'\nimport type { UserMetadata } from '../../lib/main/sqlite/models'\nimport { PaidStatus } from '../../lib/main/sqlite/models'\n\ninterface UserMetadataStore {\n  metadata: UserMetadata | null\n  isLoading: boolean\n  loadMetadata: () => Promise<void>\n  updateMetadata: (\n    updates: Partial<Omit<UserMetadata, 'id' | 'user_id' | 'created_at'>>,\n  ) => Promise<void>\n  setPaidStatus: (status: PaidStatus) => Promise<void>\n  setFreeWords: (count: number | null) => Promise<void>\n  setProTrialStartDate: (date: Date | null) => Promise<void>\n  setProTrialEndDate: (date: Date | null) => Promise<void>\n  setProSubscriptionStartDate: (date: Date | null) => Promise<void>\n  setProSubscriptionEndDate: (date: Date | null) => Promise<void>\n}\n\n// Default state for new free users\nconst DEFAULT_METADATA = {\n  paid_status: PaidStatus.FREE,\n  free_words_remaining: 4000,\n  pro_trial_start_date: null,\n  pro_trial_end_date: null,\n  pro_subscription_start_date: null,\n  pro_subscription_end_date: null,\n}\n\nexport const useUserMetadataStore = create<UserMetadataStore>((set, get) => ({\n  metadata: null,\n  isLoading: false,\n\n  loadMetadata: async () => {\n    try {\n      set({ isLoading: true })\n      const metadata = await window.api.userMetadata.get()\n\n      // If no metadata exists for this user, create default\n      if (!metadata) {\n        const { user } = useAuthStore.getState()\n        if (!user?.id) return\n\n        const now = new Date()\n        const newMetadata: UserMetadata = {\n          id: crypto.randomUUID(),\n          user_id: user.id,\n          ...DEFAULT_METADATA,\n          created_at: now,\n          updated_at: now,\n        }\n\n        await window.api.userMetadata.upsert(newMetadata)\n        set({ metadata: newMetadata, isLoading: false })\n      } else {\n        set({ metadata, isLoading: false })\n      }\n    } catch (error) {\n      console.error('Failed to load user metadata from database:', error)\n      set({ isLoading: false })\n    }\n  },\n\n  updateMetadata: async updates => {\n    try {\n      await window.api.userMetadata.update(updates)\n      await get().loadMetadata() // Reload to get fresh data\n    } catch (error) {\n      console.error('Failed to update user metadata:', error)\n      throw error\n    }\n  },\n\n  setPaidStatus: async (status: PaidStatus) => {\n    await get().updateMetadata({ paid_status: status })\n  },\n\n  setFreeWords: async (count: number | null) => {\n    await get().updateMetadata({ free_words_remaining: count })\n  },\n\n  setProTrialStartDate: async (date: Date | null) => {\n    await get().updateMetadata({ pro_trial_start_date: date })\n  },\n\n  setProTrialEndDate: async (date: Date | null) => {\n    await get().updateMetadata({ pro_trial_end_date: date })\n  },\n\n  setProSubscriptionStartDate: async (date: Date | null) => {\n    await get().updateMetadata({ pro_subscription_start_date: date })\n  },\n\n  setProSubscriptionEndDate: async (date: Date | null) => {\n    await get().updateMetadata({ pro_subscription_end_date: date })\n  },\n}))\n"
  },
  {
    "path": "app/styles/app.css",
    "content": "@import './globals.css';\n@import './window.css';\n\nbody {\n  font-family:\n    'Inter',\n    system-ui,\n    -apple-system,\n    BlinkMacSystemFont,\n    'Segoe UI',\n    Roboto,\n    sans-serif;\n  font-size: 14px;\n  margin: 0;\n  overflow: hidden;\n}\n\nhtml,\nbody,\n#app {\n  height: 100%;\n  margin: 0;\n  line-height: 1.4;\n}\n"
  },
  {
    "path": "app/styles/globals.css",
    "content": "@import 'tailwindcss';\n@source '@/app';\n@source '@/lib';\n\n@theme {\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --color-card: var(--card);\n  --color-card-foreground: var(--card-foreground);\n  --color-popover: var(--popover);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-secondary: var(--secondary);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-muted: var(--muted);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-accent: var(--accent);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-destructive: var(--destructive);\n  --color-destructive-foreground: var(--destructive-foreground);\n  --color-border: var(--border);\n  --color-input: var(--input);\n  --color-ring: var(--ring);\n  --color-chart-1: var(--chart-1);\n  --color-chart-2: var(--chart-2);\n  --color-chart-3: var(--chart-3);\n  --color-chart-4: var(--chart-4);\n  --color-chart-5: var(--chart-5);\n  --radius-lg: var(--radius);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-sm: calc(var(--radius) - 4px);\n}\n\n@layer base {\n  :root {\n    --background: var(--color-neutral-50);\n    --foreground: var(--color-neutral-900);\n    --card: hsl(0 0% 100%);\n    --card-foreground: hsl(0 0% 3.9%);\n    --popover: hsl(0 0% 100%);\n    --popover-foreground: hsl(0 0% 3.9%);\n    --primary: hsl(0 0% 9%);\n    --primary-foreground: hsl(0 0% 98%);\n    --secondary: hsl(0 0% 96.1%);\n    --secondary-foreground: hsl(0 0% 9%);\n    --muted: hsl(0 0% 96.1%);\n    --muted-foreground: hsl(0 0% 45.1%);\n    --accent: hsl(0 0% 96.1%);\n    --accent-foreground: hsl(0 0% 9%);\n    --destructive: hsl(0 84.2% 60.2%);\n    --destructive-foreground: hsl(0 0% 98%);\n    --border: hsl(0 0% 89.8%);\n    --input: hsl(0 0% 89.8%);\n    --ring: hsl(0 0% 3.9%);\n    --chart-1: hsl(12 76% 61%);\n    --chart-2: hsl(173 58% 39%);\n    --chart-3: hsl(197 37% 24%);\n    --chart-4: hsl(43 74% 66%);\n    --chart-5: hsl(27 87% 67%);\n    --radius: 0.5rem;\n  }\n\n  /* Custom scrollbar styling */\n  * {\n    scrollbar-width: thin;\n    scrollbar-color: rgb(209 213 219) transparent;\n  }\n\n  *::-webkit-scrollbar {\n    width: 8px;\n    height: 8px;\n  }\n\n  *::-webkit-scrollbar-track {\n    background: transparent;\n  }\n\n  *::-webkit-scrollbar-thumb {\n    background-color: rgb(209 213 219);\n    border-radius: 9999px;\n  }\n\n  *::-webkit-scrollbar-thumb:hover {\n    background-color: rgb(156 163 175);\n  }\n\n  .dark {\n    --background: var(--color-neutral-950);\n    --foreground: var(--color-neutral-100);\n    --card: hsl(0, 0%, 3.9%);\n    --card-foreground: hsl(0 0% 98%);\n    --popover: hsl(0 0% 3.9%);\n    --popover-foreground: hsl(0 0% 98%);\n    --primary: hsl(0 0% 98%);\n    --primary-foreground: hsl(0 0% 9%);\n    --secondary: hsl(0 0% 14.9%);\n    --secondary-foreground: hsl(0 0% 98%);\n    --muted: hsl(0 0% 14.9%);\n    --muted-foreground: hsl(0 0% 63.9%);\n    --accent: hsl(0 0% 14.9%);\n    --accent-foreground: hsl(0 0% 98%);\n    --destructive: hsl(0 62.8% 30.6%);\n    --destructive-foreground: hsl(0 0% 98%);\n    --border: hsl(0 0% 14.9%);\n    --input: hsl(0 0% 14.9%);\n    --ring: hsl(0 0% 83.1%);\n    --chart-1: hsl(220 70% 50%);\n    --chart-2: hsl(160 60% 45%);\n    --chart-3: hsl(30 80% 55%);\n    --chart-4: hsl(280 65% 60%);\n    --chart-5: hsl(340 75% 55%);\n  }\n\n  /* Dark mode scrollbar */\n  .dark * {\n    scrollbar-color: rgb(64 64 64) transparent;\n  }\n\n  .dark *::-webkit-scrollbar-thumb {\n    background-color: rgb(64 64 64);\n  }\n\n  .dark *::-webkit-scrollbar-thumb:hover {\n    background-color: rgb(82 82 82);\n  }\n}\n\n@layer utilities {\n  .scrollbar-hide {\n    -ms-overflow-style: none;\n    /* Internet Explorer 10+ */\n    scrollbar-width: none;\n    /* Firefox */\n  }\n\n  .scrollbar-hide::-webkit-scrollbar {\n    display: none;\n    /* Safari and Chrome */\n  }\n}\n"
  },
  {
    "path": "app/styles/window.css",
    "content": ":root {\n  color-scheme: light;\n  --window-icon-height: 16px;\n  --window-title-margin: 42px;\n  --window-titlebar-height: 48px;\n  --window-titlebar-font-size: 13px;\n  --window-scrollbar-width: 12px;\n  --window-mac-titlebar-controls-margin: 80px;\n  --window-background-transition-duration: 0.3s;\n  --window-c-popup-font-weight: normal;\n  --window-titlebar-font-weight: 500;\n  --window-c-popup-font-weight: 500;\n\n  --window-c-background: #fff;\n  --window-c-titlebar-background: #fff;\n  --window-c-titlebar-border: #8080801a;\n  --window-c-text: #000000c8;\n  --window-c-hover: #ececec;\n  --window-c-popup-background: #f6f6f6;\n  --window-c-popup-border: #dcdcdc;\n  --window-c-popup-shadow: hsla(0, 0%, 0%, 0.1);\n  --window-c-separator: #80808033;\n  --window-c-control-hover: #0000001a;\n  --window-c-control-close-hover: #ff453b;\n  --window-c-control-close-hover-text: #fff;\n  --window-c-scrollbar-track: #e0e0e0;\n  --window-c-scrollbar-thumb: #b0b0b0;\n  --window-c-scrollbar-thumb-hover: #888888cf;\n  --window-c-text-shadow: transparent;\n}\n\n:root.dark {\n  color-scheme: dark;\n  --window-icon-height: 16px;\n  --window-title-margin: 42px;\n  --window-titlebar-height: 48px;\n  --window-titlebar-font-size: 13px;\n  --window-scrollbar-width: 12px;\n  --window-mac-titlebar-controls-margin: 80px;\n  --window-background-transition-duration: 0.3s;\n  --window-c-popup-font-weight: normal;\n\n  --window-c-background: #1c1c1c;\n  --window-c-titlebar-background: #282828;\n  --window-c-titlebar-border: transparent;\n  --window-c-text: #ffffffc8;\n  --window-c-hover: #3c3c3c;\n  --window-c-popup-background: #282828;\n  --window-c-popup-border: #3c3c3c;\n  --window-c-popup-shadow: #00000080;\n  --window-c-separator: #80808033;\n  --window-c-control-hover: #0000003d;\n  --window-c-control-close-hover: #c42b1c;\n  --window-c-scrollbar-track: #1e1e1ec6;\n  --window-c-scrollbar-thumb: #88888863;\n  --window-c-scrollbar-thumb-hover: #555;\n  --window-c-text-shadow: #000000a8;\n}\n\n.window-frame {\n  display: flex;\n  flex-direction: column;\n  user-select: none;\n  background-color: var(--window-c-background);\n  transition: background-color var(--window-background-transition-duration) ease;\n}\n\n.window-content {\n  flex: 1;\n  overflow-y: auto;\n  overflow-x: hidden;\n  position: relative;\n}\n\n/* Assign last children to use full height of window content */\n/* .window-content div:last-child {\n  height: 100%;\n} */\n\n.window-titlebar {\n  display: flex;\n  position: relative;\n  height: var(--window-titlebar-height);\n  align-items: center;\n  -webkit-app-region: drag;\n  background-color: var(--window-c-titlebar-background);\n  color: var(--window-c-text);\n  transition: background-color var(--window-background-transition-duration) ease;\n  border-bottom: 1px solid var(--window-c-titlebar-border);\n}\n\n.titlebar-action-btn {\n  -webkit-app-region: no-drag;\n}\n\n.window-titlebar-icon {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: var(--window-title-margin);\n  height: 100%;\n  padding: 0 10px;\n  box-sizing: border-box;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.window-titlebar-icon img {\n  width: 100%;\n  max-width: 16px;\n}\n\n.window-titlebar-title {\n  flex: 1;\n  font-size: var(--window-titlebar-font-size);\n  margin-left: var(--window-title-margin);\n  font-weight: var(--window-titlebar-font-weight, normal);\n  padding-left: 4px;\n}\n\n.window-titlebar-title[data-centered] {\n  text-align: center;\n  padding-left: 0;\n  margin: 0;\n}\n\n.window-titlebar-controls {\n  display: flex;\n  position: absolute;\n  left: 0;\n  top: 0;\n  -webkit-app-region: no-drag;\n  z-index: 20;\n}\n\n.window-titlebar-menu {\n  display: flex;\n  flex-direction: row;\n  gap: 2px;\n  position: absolute;\n  top: 9px;\n  left: var(--window-title-margin);\n  -webkit-app-region: no-drag;\n  font-size: var(--window-titlebar-font-size);\n}\n\n.titlebar-menuItem .menuItem-label {\n  padding: 2px 8px;\n  cursor: pointer;\n  border-radius: 4px;\n  font-weight: var(--window-titlebar-font-weight, normal);\n}\n\n.titlebar-menuItem:hover .menuItem-label,\n.titlebar-menuItem.active .menuItem-label {\n  background-color: var(--window-c-hover);\n}\n\n.titlebar-menuItem .menuItem-popup {\n  position: fixed;\n  background-color: var(--window-c-popup-background);\n  top: 32px;\n  min-width: 100px;\n  border: 1px solid var(--window-c-popup-border);\n  padding: 0.25rem 0;\n  box-shadow: 2px 1px 4px var(--window-c-popup-shadow);\n  z-index: 10000;\n  border-radius: 4px;\n}\n\n.titlebar-menuItem .menuItem-popupItem {\n  display: flex;\n  flex-direction: row;\n  padding: 5px 18px;\n  text-shadow: 1px 1px var(--window-c-text-shadow);\n  justify-content: space-between;\n  font-weight: var(--window-c-popup-font-weight);\n}\n\n.titlebar-menuItem .menuItem-shortcut {\n  opacity: 0.5;\n  margin-left: 3rem;\n}\n\n.titlebar-menuItem .menuItem-popupItem:hover {\n  background-color: var(--window-c-hover);\n}\n\n.titlebar-menuItem .menuItem-popupItem.menuItem-separator {\n  border-top: 1px solid var(--window-c-separator);\n  margin-top: 6px;\n  padding: 3px 0;\n  pointer-events: none;\n}\n\n.titlebar-controlButton {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 36px;\n  height: 30px;\n  cursor: pointer;\n  background-color: transparent;\n}\n\n.titlebar-controlButton:hover {\n  background-color: var(--window-c-control-hover);\n}\n\n.titlebar-controlButton[aria-label='close']:hover {\n  background-color: var(--window-c-control-close-hover);\n  color: var(--window-c-control-close-hover-text, inherit);\n}\n\n/* Custom scrollbar for window content */\n.window-content::-webkit-scrollbar {\n  width: var(--window-scrollbar-width);\n}\n\n.window-content::-webkit-scrollbar-track {\n  background-color: var(--window-c-scrollbar-track);\n}\n\n.window-content::-webkit-scrollbar-thumb {\n  background-color: var(--window-c-scrollbar-thumb);\n  border-radius: 4px;\n  border: 3px solid var(--window-c-scrollbar-track);\n}\n\n.window-content::-webkit-scrollbar-thumb:hover {\n  background-color: var(--window-c-scrollbar-thumb-hover);\n}\n\n.platform-darwin .window-titlebar-title {\n  margin-left: var(--window-mac-titlebar-controls-margin);\n}\n.platform-darwin .window-titlebar-menu {\n  left: var(--window-mac-titlebar-controls-margin);\n}\n"
  },
  {
    "path": "app/utils/audioUtils.ts",
    "content": "export function createStereo48kWavFromMonoPCM(\n  pcm16le: Uint8Array,\n  srcRate = 16000,\n  targetRate = 48000,\n  bitsPerSample = 16,\n): ArrayBuffer {\n  const sampleCount = Math.floor(pcm16le.length / 2)\n  const src = new Float32Array(sampleCount)\n  for (let i = 0, j = 0; i < sampleCount; i++, j += 2) {\n    let s = (pcm16le[j] | (pcm16le[j + 1] << 8)) & 0xffff\n    if (s & 0x8000) s = s - 0x10000\n    src[i] = Math.max(-1, Math.min(1, s / 32768))\n  }\n\n  const ratio = targetRate / srcRate\n  const outLen = Math.max(1, Math.floor(src.length * ratio))\n  const resampled = new Float32Array(outLen)\n  for (let i = 0; i < outLen; i++) {\n    const pos = i / ratio\n    const idx = Math.floor(pos)\n    const frac = pos - idx\n    const a = src[idx] ?? 0\n    const b = src[idx + 1] ?? a\n    resampled[i] = a + (b - a) * frac\n  }\n\n  const numChannels = 2\n  const interleaved = new Int16Array(outLen * numChannels)\n  for (let i = 0, j = 0; i < outLen; i++) {\n    const s = Math.max(-1, Math.min(1, resampled[i]))\n    const v = (s * 32767) | 0\n    interleaved[j++] = v\n    interleaved[j++] = v\n  }\n\n  const byteRate = (targetRate * numChannels * bitsPerSample) / 8\n  const blockAlign = (numChannels * bitsPerSample) / 8\n  const dataLength = interleaved.byteLength\n  const buffer = new ArrayBuffer(44 + dataLength)\n  const view = new DataView(buffer)\n\n  const writeString = (offset: number, string: string) => {\n    for (let i = 0; i < string.length; i++) {\n      view.setUint8(offset + i, string.charCodeAt(i))\n    }\n  }\n\n  writeString(0, 'RIFF')\n  view.setUint32(4, 36 + dataLength, true)\n  writeString(8, 'WAVE')\n  writeString(12, 'fmt ')\n  view.setUint32(16, 16, true)\n  view.setUint16(20, 1, true)\n  view.setUint16(22, numChannels, true)\n  view.setUint32(24, targetRate, true)\n  view.setUint32(28, byteRate, true)\n  view.setUint16(32, blockAlign, true)\n  view.setUint16(34, bitsPerSample, true)\n  writeString(36, 'data')\n  view.setUint32(40, dataLength, true)\n\n  new Uint8Array(buffer).set(new Uint8Array(interleaved.buffer), 44)\n  return buffer\n}\n"
  },
  {
    "path": "app/utils/healthCheck.test.ts",
    "content": "import { describe, test, expect, mock, beforeEach } from 'bun:test'\nimport { checkLocalServerHealth } from './healthCheck'\n\n// Mock the window.api\nconst mockApi = {\n  checkServerHealth: mock(),\n}\n\n// Setup global window mock\nconst originalWindow = globalThis.window\nbeforeEach(() => {\n  globalThis.window = {\n    ...originalWindow,\n    api: mockApi,\n  } as any\n\n  // Clear mock call history\n  mockApi.checkServerHealth.mockClear()\n})\n\ndescribe('checkLocalServerHealth', () => {\n  test('should return healthy status when server is healthy', async () => {\n    // Arrange\n    const mockResponse = {\n      isHealthy: true,\n      error: undefined,\n    }\n    mockApi.checkServerHealth.mockResolvedValue(mockResponse)\n\n    // Act\n    const result = await checkLocalServerHealth()\n\n    // Assert\n    expect(result).toEqual({\n      isHealthy: true,\n      error: undefined,\n    })\n    expect(mockApi.checkServerHealth).toHaveBeenCalledTimes(1)\n  })\n\n  test('should return unhealthy status when server is not running', async () => {\n    // Arrange\n    const mockResponse = {\n      isHealthy: false,\n      error: 'Local server not running',\n    }\n    mockApi.checkServerHealth.mockResolvedValue(mockResponse)\n\n    // Act\n    const result = await checkLocalServerHealth()\n\n    // Assert\n    expect(result).toEqual({\n      isHealthy: false,\n      error: 'Local server not running',\n    })\n    expect(mockApi.checkServerHealth).toHaveBeenCalledTimes(1)\n  })\n\n  test('should return unhealthy status when server returns invalid response', async () => {\n    // Arrange\n    const mockResponse = {\n      isHealthy: false,\n      error: 'Invalid server response',\n    }\n    mockApi.checkServerHealth.mockResolvedValue(mockResponse)\n\n    // Act\n    const result = await checkLocalServerHealth()\n\n    // Assert\n    expect(result).toEqual({\n      isHealthy: false,\n      error: 'Invalid server response',\n    })\n    expect(mockApi.checkServerHealth).toHaveBeenCalledTimes(1)\n  })\n\n  test('should handle API call errors gracefully', async () => {\n    // Arrange\n    const error = new Error('IPC communication failed')\n    mockApi.checkServerHealth.mockRejectedValue(error)\n\n    // Act\n    const result = await checkLocalServerHealth()\n\n    // Assert\n    expect(result).toEqual({\n      isHealthy: false,\n      error: 'IPC communication failed',\n    })\n    expect(mockApi.checkServerHealth).toHaveBeenCalledTimes(1)\n  })\n\n  test('should handle unknown error types', async () => {\n    // Arrange\n    mockApi.checkServerHealth.mockRejectedValue('Unknown error')\n\n    // Act\n    const result = await checkLocalServerHealth()\n\n    // Assert\n    expect(result).toEqual({\n      isHealthy: false,\n      error: 'Unknown error occurred',\n    })\n    expect(mockApi.checkServerHealth).toHaveBeenCalledTimes(1)\n  })\n\n  test('should handle timeout scenarios', async () => {\n    // Arrange\n    const mockResponse = {\n      isHealthy: false,\n      error: 'Connection timed out',\n    }\n    mockApi.checkServerHealth.mockResolvedValue(mockResponse)\n\n    // Act\n    const result = await checkLocalServerHealth()\n\n    // Assert\n    expect(result).toEqual({\n      isHealthy: false,\n      error: 'Connection timed out',\n    })\n    expect(mockApi.checkServerHealth).toHaveBeenCalledTimes(1)\n  })\n})\n"
  },
  {
    "path": "app/utils/healthCheck.ts",
    "content": "/**\n * Simple health check utility for the local Ito server using the main process\n */\n\nexport interface HealthCheckResult {\n  isHealthy: boolean\n  error?: string\n}\n\n/**\n * Performs a health check against the local Ito server via the main process\n * This avoids CORS issues by using the main process to make the HTTP request\n * @returns Promise resolving to health check result\n */\nexport async function checkLocalServerHealth(): Promise<HealthCheckResult> {\n  try {\n    // Use the main process to check server health via HTTP\n    const result = await window.api.checkServerHealth()\n\n    return {\n      isHealthy: result.isHealthy,\n      error: result.error,\n    }\n  } catch (error) {\n    const errorMessage =\n      error instanceof Error ? error.message : 'Unknown error occurred'\n\n    return {\n      isHealthy: false,\n      error: errorMessage,\n    }\n  }\n}\n"
  },
  {
    "path": "app/utils/keyboard.test.ts",
    "content": "import { describe, test, expect, beforeEach, mock } from 'bun:test'\nimport { KeyState } from './keyboard'\nimport type { KeyEvent } from '@/lib/preload'\n\n// Mock the window.api for KeyState tests\nconst mockApi = {\n  blockKeys: mock(),\n}\n\nglobal.window = {\n  api: mockApi as any,\n} as any\n\nbeforeEach(() => {\n  mockApi.blockKeys.mockClear()\n})\n\ndescribe('KeyState', () => {\n  let keyState: KeyState\n\n  beforeEach(() => {\n    keyState = new KeyState()\n  })\n\n  describe('constructor', () => {\n    test('should initialize with empty shortcut by default', () => {\n      const state = new KeyState()\n      expect(state.getPressedKeys()).toEqual([])\n    })\n\n    test('should initialize with provided shortcut', () => {\n      const keyState = new KeyState(['command', 'space'])\n\n      expect(keyState).toBeDefined()\n    })\n  })\n\n  describe('updateShortcut', () => {\n    test('should update the shortcut', () => {\n      keyState.updateShortcut(['command', 'z'])\n\n      expect(keyState).toBeDefined()\n    })\n\n    test('should handle empty shortcut', () => {\n      keyState.updateShortcut([])\n\n      expect(keyState).toBeDefined()\n    })\n  })\n\n  describe('update', () => {\n    test('should track keydown events', () => {\n      keyState.update({ key: 'KeyA', type: 'keydown' } as KeyEvent)\n      expect(keyState.getPressedKeys()).toContain('a')\n      expect(keyState.isKeyPressed('a')).toBe(true)\n    })\n\n    test('should track keyup events', () => {\n      keyState.update({ key: 'KeyA', type: 'keydown' } as KeyEvent)\n      keyState.update({ key: 'KeyA', type: 'keyup' } as KeyEvent)\n      expect(keyState.getPressedKeys()).not.toContain('a')\n      expect(keyState.isKeyPressed('a')).toBe(false)\n    })\n\n    test('should ignore fn_fast events', () => {\n      keyState.update({ key: 'Unknown(179)', type: 'keydown' } as KeyEvent)\n      expect(keyState.getPressedKeys()).toEqual([])\n    })\n\n    test('should track multiple keys', () => {\n      keyState.update({ key: 'KeyA', type: 'keydown' } as KeyEvent)\n      keyState.update({ key: 'KeyB', type: 'keydown' } as KeyEvent)\n      expect(keyState.getPressedKeys()).toContain('a')\n      expect(keyState.getPressedKeys()).toContain('b')\n      expect(keyState.getPressedKeys()).toHaveLength(2)\n    })\n  })\n\n  describe('getPressedKeys', () => {\n    test('should return empty array initially', () => {\n      expect(keyState.getPressedKeys()).toEqual([])\n    })\n\n    test('should return currently pressed keys', () => {\n      keyState.update({ key: 'KeyA', type: 'keydown' } as KeyEvent)\n      keyState.update({ key: 'Space', type: 'keydown' } as KeyEvent)\n      const pressed = keyState.getPressedKeys()\n      expect(pressed).toContain('a')\n      expect(pressed).toContain('space')\n      expect(pressed).toHaveLength(2)\n    })\n  })\n\n  describe('isKeyPressed', () => {\n    test('should return false for unpressed keys', () => {\n      expect(keyState.isKeyPressed('a')).toBe(false)\n    })\n\n    test('should return true for pressed keys', () => {\n      keyState.update({ key: 'KeyA', type: 'keydown' } as KeyEvent)\n      expect(keyState.isKeyPressed('a')).toBe(true)\n    })\n  })\n\n  describe('clear', () => {\n    test('should clear all pressed keys', () => {\n      keyState.update({ key: 'KeyA', type: 'keydown' } as KeyEvent)\n      keyState.update({ key: 'KeyB', type: 'keydown' } as KeyEvent)\n      keyState.clear()\n      expect(keyState.getPressedKeys()).toEqual([])\n      expect(keyState.isKeyPressed('a')).toBe(false)\n      expect(keyState.isKeyPressed('b')).toBe(false)\n    })\n\n    test('should clear all pressed keys', () => {\n      keyState.update({ key: 'KeyA', type: 'keydown' } as KeyEvent)\n      keyState.clear()\n      expect(keyState.getPressedKeys()).toHaveLength(0)\n    })\n  })\n\n  describe('key blocking behavior', () => {\n    test('should track non-shortcut keys correctly', () => {\n      keyState.updateShortcut(['command', 'z'])\n\n      keyState.update({ key: 'KeyA', type: 'keydown' } as KeyEvent)\n      expect(keyState.getPressedKeys()).toContain('a')\n    })\n\n    test('should track keys when part of shortcut is pressed', () => {\n      keyState.updateShortcut(['command', 'z'])\n\n      keyState.update({ key: 'MetaLeft', type: 'keydown' } as KeyEvent)\n      expect(keyState.isKeyPressed('command-left')).toBe(true)\n      expect(keyState.getPressedKeys()).toContain('command-left')\n    })\n\n    test('should track keys when complete shortcut is pressed', () => {\n      keyState.updateShortcut(['command', 'z'])\n\n      keyState.update({ key: 'MetaLeft', type: 'keydown' } as KeyEvent)\n      keyState.update({ key: 'KeyZ', type: 'keydown' } as KeyEvent)\n      expect(keyState.isKeyPressed('command-left')).toBe(true)\n      expect(keyState.isKeyPressed('z')).toBe(true)\n    })\n\n    test('should track key releases correctly', () => {\n      keyState.updateShortcut(['command', 'z'])\n      keyState.update({ key: 'MetaLeft', type: 'keydown' } as KeyEvent)\n\n      keyState.update({ key: 'MetaLeft', type: 'keyup' } as KeyEvent)\n      expect(keyState.isKeyPressed('command-left')).toBe(false)\n    })\n\n    test('should handle complex shortcuts with multiple modifier keys', () => {\n      keyState.updateShortcut(['command', 'shift', 'z'])\n\n      keyState.update({ key: 'MetaLeft', type: 'keydown' } as KeyEvent)\n      keyState.update({ key: 'ShiftLeft', type: 'keydown' } as KeyEvent)\n\n      expect(keyState.isKeyPressed('command-left')).toBe(true)\n      expect(keyState.isKeyPressed('shift-left')).toBe(true)\n    })\n\n    test('should handle fn key in shortcuts', () => {\n      keyState.updateShortcut(['fn', 'f1'])\n\n      keyState.update({ key: 'Function', type: 'keydown' } as KeyEvent)\n\n      // Should track fn key presses\n      expect(keyState.isKeyPressed('fn')).toBe(true)\n    })\n\n    test('should track command keys correctly', () => {\n      keyState.updateShortcut(['command'])\n\n      keyState.update({ key: 'MetaLeft', type: 'keydown' } as KeyEvent)\n\n      // Should track the command key (as command-left)\n      expect(keyState.isKeyPressed('command-left')).toBe(true)\n      expect(keyState.getPressedKeys()).toContain('command-left')\n    })\n  })\n\n  describe('edge cases', () => {\n    test('should handle same key pressed multiple times', () => {\n      keyState.update({ key: 'KeyA', type: 'keydown' } as KeyEvent)\n      keyState.update({ key: 'KeyA', type: 'keydown' } as KeyEvent)\n      expect(keyState.getPressedKeys()).toEqual(['a'])\n    })\n\n    test('should handle keyup for unpressed key', () => {\n      keyState.update({ key: 'KeyA', type: 'keyup' } as KeyEvent)\n      expect(keyState.getPressedKeys()).toEqual([])\n    })\n\n    test('should handle shortcut change while keys are pressed', () => {\n      keyState.updateShortcut(['command', 'z'])\n      keyState.update({ key: 'MetaLeft', type: 'keydown' } as KeyEvent)\n\n      // Change shortcut while command is still pressed\n      keyState.updateShortcut(['command', 'x'])\n\n      // KeyState should still track the pressed key correctly\n      expect(keyState.isKeyPressed('command-left')).toBe(true)\n    })\n  })\n})\n"
  },
  {
    "path": "app/utils/keyboard.ts",
    "content": "import { KeyEvent } from '@/lib/preload'\nimport { KeyboardShortcutConfig } from '@/lib/main/store'\nimport { ItoMode } from '../generated/ito_pb'\nimport {\n  keyNameMap,\n  normalizeLegacyKey,\n  getKeyDisplayInfo,\n  KeyName,\n} from '@/lib/types/keyboard'\n\n/**\n * Helper to format directional indicators for modifier keys\n */\nexport function getDirectionalIndicator(\n  side: 'left' | 'right' | undefined,\n  showText: boolean = false,\n): string {\n  if (!side) return ''\n  const arrow = side === 'left' ? '◀' : '▶'\n  if (showText) {\n    return side === 'left' ? `${arrow} left` : `right ${arrow}`\n  }\n  return arrow\n}\n\n/**\n * Get formatted display components for a key\n * @param keyboardKey The key name to display\n * @param platform The platform to render keys for\n * @param options Display options\n * @returns Object with formatted display components\n */\nexport function getKeyDisplay(\n  keyboardKey: KeyName,\n  platform: 'darwin' | 'win32' | undefined = 'darwin',\n  options: {\n    showDirectionalText?: boolean\n    format?: 'symbol' | 'label' | 'both'\n  } = {},\n): string {\n  const { showDirectionalText = false, format = 'symbol' } = options\n\n  const displayInfo = getKeyDisplayInfo(keyboardKey, platform)\n  const dirIndicator = getDirectionalIndicator(\n    displayInfo.side,\n    showDirectionalText,\n  )\n\n  const label = displayInfo.label\n\n  let result: string\n  if (displayInfo.isModifier && displayInfo.symbol) {\n    if (format === 'symbol') {\n      result = displayInfo.symbol\n      if (dirIndicator) {\n        result = showDirectionalText\n          ? `${result} ${dirIndicator}`\n          : `${result} ${dirIndicator}`\n      }\n    } else if (format === 'label') {\n      result = label\n      if (dirIndicator) {\n        result = showDirectionalText\n          ? `${result} ${dirIndicator}`\n          : `${result} ${dirIndicator}`\n      }\n    } else {\n      // 'both'\n      result = `${displayInfo.symbol} ${label}`\n      if (dirIndicator) {\n        result = `${result} ${dirIndicator}`\n      }\n    }\n  } else {\n    result = label\n  }\n\n  return result\n}\n\nexport type ShortcutError =\n  | 'duplicate-key-same-mode'\n  | 'duplicate-key-diff-mode'\n  | 'not-found'\n  | 'reserved-combination'\n\nexport type ShortcutResult = {\n  success: boolean\n  error?: ShortcutError\n  errorMessage?: string\n}\n\nconst MODIFIER_SEQUENCE = [\n  'control',\n  'control-left',\n  'control-right',\n  'option',\n  'option-left',\n  'option-right',\n  'alt',\n  'shift',\n  'shift-left',\n  'shift-right',\n  'command',\n  'command-left',\n  'command-right',\n  'fn',\n] as const\n\nconst MODIFIER_INDEX: Record<string, number> = MODIFIER_SEQUENCE.reduce(\n  (acc, key, i) => {\n    acc[key] = i\n    return acc\n  },\n  {} as Record<string, number>,\n)\n\nfunction normalizeKey(raw: KeyName): KeyName {\n  return raw.trim().toLowerCase() as KeyName\n}\n\nfunction sortKeysCanonical(keys: KeyName[]): KeyName[] {\n  const unique = Array.from(new Set(keys.map(normalizeKey)))\n\n  const modifiers: KeyName[] = []\n  const nonModifiers: KeyName[] = []\n\n  for (const key of unique) {\n    if (key in MODIFIER_INDEX) modifiers.push(key)\n    else nonModifiers.push(key)\n  }\n\n  modifiers.sort((a, b) => MODIFIER_INDEX[a] - MODIFIER_INDEX[b])\n  nonModifiers.sort() // simple alphabetical for everything else\n\n  return [...modifiers, ...nonModifiers]\n}\n\nexport function normalizeChord(keys: KeyName[]): KeyName[] {\n  return sortKeysCanonical(keys.filter(Boolean))\n}\n\n// Helper to generate all variants of a modifier key (base, left, right)\nfunction modifierVariants(modifier: string): string[] {\n  return [modifier, `${modifier}-left`, `${modifier}-right`]\n}\n\n// Helper to create reserved combinations for all variants of a modifier\nfunction createReservedCombos(\n  modifier: string,\n  key: string | null,\n  reason: string,\n) {\n  return modifierVariants(modifier).map(mod => ({\n    keys: key ? [mod as KeyName, key as KeyName] : [mod as KeyName],\n    reason,\n  }))\n}\n\n// Get platform-specific reserved combinations\nfunction getReservedCombinations(\n  platform: 'darwin' | 'win32' = 'darwin',\n): { keys: KeyName[]; reason?: string }[] {\n  // Common combinations across all platforms\n  const common = [\n    // Browser tab switching (works the same on all platforms)\n    ...createReservedCombos('control', 'tab', 'Browser tab switching'),\n  ]\n\n  if (platform === 'darwin') {\n    // macOS uses Command key for system operations\n    return [\n      ...common,\n      ...createReservedCombos('command', 'c', 'Reserved for copying'),\n      ...createReservedCombos('command', 'v', 'Reserved for pasting'),\n      ...createReservedCombos('command', 'q', 'System quit command'),\n      ...createReservedCombos('command', 'w', 'Close window'),\n      ...createReservedCombos('command', 'tab', 'System app switching'),\n    ]\n  } else {\n    // Windows uses Control for copy/paste, Alt (option) for app switching\n    return [\n      ...common,\n      ...createReservedCombos('control', 'c', 'Reserved for copying'),\n      ...createReservedCombos('control', 'v', 'Reserved for pasting'),\n      ...createReservedCombos('control', 'w', 'Close tab/window'),\n      ...createReservedCombos(\n        'option',\n        'tab',\n        'System app switching (Alt+Tab)',\n      ),\n    ]\n  }\n}\n\n// Check if a shortcut contains reserved key combinations\nexport function isReservedCombination(\n  keys: KeyName[],\n  platform: 'darwin' | 'win32' = 'darwin',\n): {\n  isReserved: boolean\n  reason?: string\n} {\n  // Normalize legacy keys to new format\n  const normalizedKeys = sortKeysCanonical(keys.map(normalizeLegacyKey))\n\n  // Get platform-specific reserved combinations\n  const reservedCombinations = getReservedCombinations(platform)\n\n  for (const reserved of reservedCombinations) {\n    const normalizedReserved = sortKeysCanonical(reserved.keys)\n\n    // Check for exact match - same number of keys and all keys match\n    const isExactMatch =\n      normalizedKeys.length === normalizedReserved.length &&\n      normalizedReserved.every(reservedKey => {\n        return normalizedKeys.includes(reservedKey)\n      })\n\n    if (isExactMatch) {\n      return { isReserved: true, reason: reserved.reason }\n    }\n  }\n\n  return { isReserved: false }\n}\n\n// Returns the mode of the duplicate shortcut if found, otherwise undefined\nexport function isDuplicateShortcut(\n  currentShortcuts: KeyboardShortcutConfig[],\n  shortcutToCheck: KeyboardShortcutConfig,\n): ItoMode | undefined {\n  // Normalize keys for comparison\n  const normalizedCheckKeys = sortKeysCanonical(\n    shortcutToCheck.keys.map(normalizeLegacyKey),\n  )\n\n  const duplicate = currentShortcuts.find(ks => {\n    if (ks.id === shortcutToCheck.id) return false\n\n    const normalizedStoredKeys = sortKeysCanonical(\n      ks.keys.map(normalizeLegacyKey),\n    )\n\n    // Check if all keys match exactly\n    return (\n      JSON.stringify(normalizedCheckKeys) ===\n      JSON.stringify(normalizedStoredKeys)\n    )\n  })\n\n  if (duplicate) {\n    return duplicate.mode\n  }\n\n  return undefined\n}\n\n// Helper to validate duplicate shortcuts and return appropriate error result\nexport function validateShortcutForDuplicate(\n  currentShortcuts: KeyboardShortcutConfig[],\n  shortcutToCheck: KeyboardShortcutConfig,\n  expectedMode: ItoMode,\n): ShortcutResult | null {\n  const duplicateMode = isDuplicateShortcut(currentShortcuts, shortcutToCheck)\n\n  if (duplicateMode !== undefined) {\n    const sameMode = duplicateMode === expectedMode\n    return {\n      success: false,\n      error: sameMode ? 'duplicate-key-same-mode' : 'duplicate-key-diff-mode',\n    }\n  }\n\n  return null // No duplicate found, validation passes\n}\n\n/**\n * Tracks the state of currently pressed keys\n */\nexport class KeyState {\n  private pressedKeys: Set<KeyName> = new Set()\n  private shortcut: KeyName[] = []\n\n  constructor(shortcut: KeyName[] = []) {\n    this.updateShortcut(shortcut)\n  }\n\n  /**\n   * Updates the shortcut\n   * @param shortcut The shortcut to set, as an array of normalized key names.\n   */\n  updateShortcut(shortcut: KeyName[]) {\n    // Normalize legacy keys to new format\n    this.shortcut = shortcut.map(normalizeLegacyKey)\n  }\n\n  /**\n   * Updates the key state based on a key event\n   * @param event The key event from the global key listener\n   */\n  update(event: KeyEvent) {\n    // Use keyNameMap for proper directional key preservation\n    const key = keyNameMap[event.key] || event.key.toLowerCase()\n\n    // Handle Function key special case\n    if (key === 'fn_fast') {\n      return\n    }\n\n    if (event.type === 'keydown') {\n      this.pressedKeys.add(key)\n    } else if (event.type === 'keyup') {\n      this.pressedKeys.delete(key)\n    }\n  }\n\n  /**\n   * Gets the currently pressed keys\n   * @returns Array of currently pressed key names\n   */\n  getPressedKeys(): string[] {\n    return Array.from(this.pressedKeys)\n  }\n\n  /**\n   * Checks if a specific key is currently pressed\n   * @param key The normalized key name to check\n   * @returns Whether the key is currently pressed\n   */\n  isKeyPressed(key: KeyName): boolean {\n    return this.pressedKeys.has(key)\n  }\n\n  /**\n   * Clears all pressed keys\n   */\n  clear() {\n    this.pressedKeys.clear()\n  }\n}\n"
  },
  {
    "path": "app/utils/utils.ts",
    "content": "export const isValidEmail = (email: string): boolean => {\n  const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n  return emailRegex.test(email)\n}\n\n// Auth0 enforces these rules\nexport const isStrongPassword = (password: string): boolean => {\n  if (!password) return false // non-empty\n  if (password.length < 8) return false // length\n  if (!/[a-z]/.test(password)) return false // lower\n  if (!/[A-Z]/.test(password)) return false // upper\n  if (!/\\d/.test(password)) return false // number\n  return true\n}\n"
  },
  {
    "path": "build/entitlements.mac.inherit.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n    <true/>\n    <key>com.apple.security.device.audio-input</key>\n    <true/>\n    <key>com.apple.security.automation.apple-events</key>\n    <true/>\n    <key>com.apple.security.network.client</key>\n    <true/>\n</dict>\n</plist> "
  },
  {
    "path": "build/entitlements.mac.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n    <true/>\n    <key>com.apple.security.device.audio-input</key>\n    <true/>\n    <key>com.apple.security.automation.apple-events</key>\n    <true/>\n    <key>com.apple.security.network.client</key>\n    <true/>\n</dict>\n</plist> "
  },
  {
    "path": "build-app.sh",
    "content": "#!/bin/bash\n\n# Exit on error\nset -e\n\n# Load environment variables from .env file if it exists\nif [ -f .env ]; then\n  export $(grep -v '^#' .env | sed 's/#.*//' | xargs)\nfi\n\n# Colors for output\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nBLUE='\\033[0;34m'\nNC='\\033[0m' # No Color\n\n# Function to print status\nprint_status() {\n    echo -e \"${GREEN}==>${NC} $1\"\n}\n\n# Function to print info\nprint_info() {\n    echo -e \"${BLUE}==>${NC} $1\"\n}\n\n# Function to print warning\nprint_warning() {\n    echo -e \"${YELLOW}==>${NC} $1\"\n}\n\n# Function to print error\nprint_error() {\n    echo -e \"${RED}Error:${NC} $1\"\n}\n\n# Clear output directory\nclear_output_dir() {\n    print_status \"Clearing output directory...\"\n    \n    if [ -d \"dist\" ]; then\n        print_info \"Removing existing dist directory...\"\n        rm -rf dist\n    fi\n    \n    print_info \"Output directory cleared\"\n}\n\n# Load NVM and Node.js environment\nsetup_node_env() {\n    print_info \"Setting up Node.js environment...\"\n    export NVM_DIR=\"$HOME/.nvm\"\n    if [ -d \"$NVM_DIR\" ] && [ -s \"$NVM_DIR/nvm.sh\" ]; then\n        \\. \"$NVM_DIR/nvm.sh\"\n        [ -s \"$NVM_DIR/bash_completion\" ] && \\. \"$NVM_DIR/bash_completion\"\n        print_info \"NVM environment loaded\"\n    else\n        print_info \"NVM not found or using system Node.js\"\n    fi\n}\n\n# Load Rust environment\nsetup_rust_env() {\n    print_info \"Setting up Rust environment...\"\n    if [ -s \"$HOME/.cargo/env\" ]; then\n        source \"$HOME/.cargo/env\"\n    else\n        print_info \"Rust environment file not found, using system Rust\"\n    fi\n}\n\n# Check if required tools are installed\ncheck_prerequisites() {\n    print_status \"Checking prerequisites...\"\n    \n    if ! command -v node &> /dev/null; then\n        print_error \"Node.js is not installed or not in PATH\"\n        exit 1\n    fi\n    \n    if ! command -v bun &> /dev/null; then\n        print_error \"Bun is not installed or not in PATH\"\n        exit 1\n    fi\n    \n    if ! command -v rustc &> /dev/null; then\n        print_error \"Rust is not installed or not in PATH\"\n        exit 1\n    fi\n    \n    if ! command -v cargo &> /dev/null; then\n        print_error \"Cargo is not installed or not in PATH\"\n        exit 1\n    fi\n    \n    print_info \"Node.js version: $(node --version)\"\n    print_info \"Bun version: $(bun --version)\"\n    print_info \"Rust version: $(rustc --version)\"\n    print_info \"Cargo version: $(cargo --version)\"\n}\n\n# Build native Rust modules\nbuild_native_modules() {\n    local platform=$1\n    print_status \"Building native Rust modules for $platform...\"\n    \n    case $platform in\n        \"mac\")\n            # Build for both architectures for release\n            ./build-binaries.sh --mac\n            ./build-binaries.sh --mac --x64\n            ;;\n        \"windows\")\n            ./build-binaries.sh --windows\n            ;;\n        \"all\")\n            # Build for all platforms and architectures for release\n            ./build-binaries.sh --mac\n            ./build-binaries.sh --mac --x64\n            ./build-binaries.sh --windows\n            ;;\n        *)\n            print_error \"Invalid platform: $platform. Use 'mac', 'windows', or 'all'\"\n            exit 1\n            ;;\n    esac\n    \n    print_status \"Native modules built successfully!\"\n}\n\n# Build Electron application\nbuild_electron_app() {\n    print_status \"Building Electron application...\"\n    \n    # Install dependencies if node_modules doesn't exist\n    if [ ! -d \"node_modules\" ]; then\n        print_info \"Installing dependencies...\"\n        bun install\n    fi\n    \n    # Build the application using electron-vite\n    print_info \"Building application with Electron Vite...\"\n    bun run electron-vite build\n    \n    print_status \"Electron application built successfully!\"\n}\n\n# Create macOS DMG installer\ncreate_dmg() {\n    print_status \"Creating macOS DMG installer...\"\n    \n    # Determine stage and handle notarization: only enforce in prod\n    local stage=\"${ITO_ENV:-prod}\"\n    if [ \"$stage\" = \"prod\" ]; then\n      if [ -z \"$APPLE_ID\" ] || [ -z \"$APPLE_APP_SPECIFIC_PASSWORD\" ] || [ -z \"$APPLE_TEAM_ID\" ]; then\n        print_error \"Prod build requires notarization credentials (APPLE_ID, APPLE_TEAM_ID, APPLE_APP_SPECIFIC_PASSWORD).\"\n        exit 1\n      else\n        print_info \"Prod build: notarization credentials found.\"\n      fi\n    else\n      print_info \"Non-prod build ('$stage'): skipping notarization and code signing auto-discovery.\"\n      export CSC_IDENTITY_AUTO_DISCOVERY=false\n    fi\n    \n    print_info \"Packaging application with Electron Builder (forcing DMG target)...\"\n    # Ensure Vite embeds the stage for runtime\n    if [ -z \"${VITE_ITO_ENV}\" ]; then\n      export VITE_ITO_ENV=\"${ITO_ENV:-dev}\"\n      print_info \"Set VITE_ITO_ENV=${VITE_ITO_ENV} for build-time embedding\"\n    fi\n    bun run electron-vite build\n    bunx electron-builder --config electron-builder.config.js --mac dmg zip --universal --publish=never\n    \n    print_status \"macOS DMG installer created successfully!\"\n    \n    # Show output location and stage-specific DMG name\n    if [ -d \"dist\" ]; then\n        print_info \"Build output location: $(pwd)/dist\"\n        local dmg_name\n        if [ \"$stage\" = \"prod\" ]; then\n          dmg_name=\"Ito-Installer.dmg\"\n        else\n          dmg_name=\"Ito-${stage}-Installer.dmg\"\n        fi\n        ls -la \"dist/${dmg_name}\" 2>/dev/null || print_warning \"${dmg_name} not found in dist directory\"\n    fi\n}\n\n# Create Windows installer\ncreate_windows_installer() {\n    print_status \"Creating Windows installer...\"\n    \n    print_info \"Packaging application with Electron Builder...\"\n    bun run electron-vite build\n    \n    # Set npm config to avoid symlink issues on Windows\n    export npm_config_cache=$PWD/.npm-cache\n    export ELECTRON_BUILDER_CACHE=$PWD/.electron-builder-cache  \n    \n    # Disable code signing completely\n    export CSC_IDENTITY_AUTO_DISCOVERY=false\n    export CSC_LINK=\"\"\n    export CSC_KEY_PASSWORD=\"\"\n    export SKIP_SIGNING=true\n    export WIN_CSC_LINK=\"\"\n    \n    print_info \"Using Docker for Windows build on $OSTYPE...\"\n    \n    # Check if Docker is available\n    if ! command -v docker &> /dev/null; then\n        print_error \"Docker is not installed. Please install Docker Desktop.\"\n        exit 1\n    fi\n    \n    # Check if Docker is running (skip in CI environments)\n    if [ -z \"$CI\" ] && ! docker info &> /dev/null; then\n        print_error \"Docker is not running. Please start Docker.\"\n        exit 1\n    fi\n    \n    # Use Docker for cross-compilation with ARM64 compatibility and bun\n    # Get absolute path in a cross-platform way\n    if [[ \"$OSTYPE\" == \"msys\" ]]; then\n        # On MinGW/MSYS2, convert to Windows path format for Docker\n        PROJECT_PATH=\"$(cygpath -w \"$(pwd)\" | sed 's|\\\\|/|g')\"\n    else\n        PROJECT_PATH=\"$(pwd)\"\n    fi\n    \n    docker run --rm --platform linux/amd64 \\\n      --env CSC_IDENTITY_AUTO_DISCOVERY=false \\\n      --env SKIP_SIGNING=true \\\n      --env VITE_ITO_VERSION=\"${VITE_ITO_VERSION}\" \\\n      --env ITO_ENV=\"${ITO_ENV}\" \\\n      -v \"${PROJECT_PATH}\":/project \\\n      electronuserland/builder:wine \\\n      bash -c \"\n        # Install bun with retry\n        curl -fsSL https://bun.sh/install | bash || curl -fsSL https://bun.sh/install | bash\n        export PATH=\\\"/root/.bun/bin:\\$PATH\\\"\n        \n        # Verify bun installation\n        bun --version\n\n        # Change to project and debug file paths\n        cd /project\n        echo 'Current directory:' \\$(pwd)\n        echo 'Directory contents:'\n        ls -la\n        \n        # Install dependencies (let SQLite3 use prebuilt binaries for Electron)\n        export npm_config_target_platform=win32\n        export npm_config_target_arch=x64\n        export npm_config_runtime=electron\n        export npm_config_sqlite3_binary_host_mirror=https://github.com/mapbox/node-sqlite3/releases/download\n        export npm_config_electron_version=\\$(node -p \\\"require('./package.json').devDependencies.electron.replace('^', '')\\\")\n        bun install || bun install --force || bun install\n        \n        # Run electron-builder\n        bunx electron-builder --config electron-builder.config.js --win --x64 --publish=never\n\n        # Copy versioned installer to static name for CDN (supports prod and dev names)\n        exe_path=\\$(ls -t dist/Ito*.exe 2>/dev/null | head -n 1)\n        if [ -n \\\"\\$exe_path\\\" ]; then\n          dest_name=\\$([ \\\"\\${ITO_ENV:-dev}\\\" = \\\"prod\\\" ] && echo \\\"Ito-Installer.exe\\\" || echo \\\"Ito-\\${ITO_ENV}-Installer.exe\\\")\n          echo \\\"Copying \\$exe_path to dist/\\$dest_name for CDN\\\"\n          cp \\\"\\$exe_path\\\" \\\"dist/\\$dest_name\\\"\n        else\n          echo 'No Windows installer .exe found to copy'\n        fi\n      \"\n    \n    print_status \"Windows installer created successfully!\"\n    \n    # Show output location\n    if [ -d \"dist\" ]; then\n        print_info \"Build output location: $(pwd)/dist\"\n        ls -la dist/*.exe 2>/dev/null || print_warning \"No .exe files found in dist directory\"\n        ls -la dist/*.nsis.7z 2>/dev/null || print_warning \"No .nsis.7z files found in dist directory\"\n    fi\n}\n\n# Show usage information\nshow_usage() {\n    echo \"Usage: $0 [PLATFORM] [OPTIONS]\"\n    echo \"\"\n    echo \"PLATFORMS:\"\n    echo \"  mac, macos          Build for macOS (default)\"\n    echo \"  win, windows        Build for Windows\"\n    echo \"\"\n    echo \"OPTIONS:\"\n    echo \"  --skip-binaries     Skip building native Rust modules\"\n    echo \"  --help, -h          Show this help message\"\n    echo \"\"\n    echo \"Examples:\"\n    echo \"  $0                  # Build for macOS\"\n    echo \"  $0 mac              # Build for macOS\"\n    echo \"  $0 windows          # Build for Windows\"\n    echo \"  $0 mac --skip-binaries    # Build macOS without rebuilding Rust modules\"\n}\n\n# Main build function\nmain() {\n    # Parse command line arguments\n    PLATFORM=\"mac\"  # default platform\n    SKIP_BINARIES=false\n    \n    for arg in \"$@\"; do\n        case $arg in\n            \"mac\"|\"macos\")\n                PLATFORM=\"mac\"\n                shift\n                ;;\n            \"win\"|\"windows\")\n                PLATFORM=\"windows\"\n                shift\n                ;;\n            --skip-binaries)\n                SKIP_BINARIES=true\n                shift\n                ;;\n            --help|-h)\n                show_usage\n                exit 0\n                ;;\n            *)\n                # Unknown option\n                print_warning \"Unknown option: $arg\"\n                ;;\n        esac\n    done\n    \n    print_status \"Starting Ito $PLATFORM build process...\"\n    echo\n    \n    # Clear output directory first\n    clear_output_dir\n    echo\n    \n    # In CI, the environment is set up by the workflow.\n    if [ -z \"$CI\" ]; then\n        # Setup environments\n        setup_node_env\n        setup_rust_env\n    fi\n    \n    # Check prerequisites\n    check_prerequisites\n    echo\n    \n    # Build native modules (unless skipped)\n    if [ \"$SKIP_BINARIES\" = false ]; then\n        case $PLATFORM in\n            \"mac\")\n                build_native_modules \"mac\"\n                ;;\n            \"windows\")\n                build_native_modules \"windows\"\n                ;;\n        esac\n        echo\n    else\n        print_info \"Skipping native modules build (--skip-binaries flag passed)\"\n        echo\n    fi\n    \n    # Build for the specified platform(s)\n    case $PLATFORM in\n        \"mac\")\n            create_dmg\n            echo\n            print_status \"macOS build process completed successfully! 🎉\"\n            if [ -z \"${ITO_ENV}\" ] || [ \"${ITO_ENV}\" = \"prod\" ]; then\n              print_info \"Your DMG installer is ready: dist/Ito-Installer.dmg\"\n            else\n              print_info \"Your DMG installer is ready: dist/Ito-${ITO_ENV}-Installer.dmg\"\n            fi\n            ;;\n        \"windows\")\n            create_windows_installer\n            echo\n            print_status \"Windows build process completed successfully! 🎉\"\n            print_info \"Your Windows installer is ready in the dist/ directory\"\n            ;;\n    esac\n}\n\n# Run main function\nmain \"$@\""
  },
  {
    "path": "build-binaries.sh",
    "content": "#!/bin/bash\n\n# Exit immediately if a command exits with a non-zero status.\nset -e\n\n# --- Color Definitions for pretty printing ---\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nBLUE='\\033[0;34m'\nNC='\\033[0m' # No Color\n\n# --- Function Definitions ---\nprint_status() {\n    echo -e \"${GREEN}==>${NC} $1\"\n}\n\nprint_info() {\n    echo -e \"${BLUE}-->${NC} $1\"\n}\n\nprint_error() {\n    echo -e \"${RED}Error:${NC} $1\" >&2\n}\n\n# --- Build the entire native workspace ---\nbuild_native_workspace() {\n    print_status \"Building native workspace...\"\n\n    # Change into the native workspace directory\n    cd \"native\"\n\n    # Check if we're compiling on a Windows machine\n    compiling_on_windows=false\n    if [[ \"$OSTYPE\" == \"msys\" ]] || [[ \"$OSTYPE\" == \"win32\" ]] || [[ \"$OS\" == \"Windows_NT\" ]]; then\n        compiling_on_windows=true\n    fi\n\n    # Install dependencies\n    print_info \"Installing dependencies for workspace...\"\n    cargo fetch\n\n    # --- macOS Build ---\n    if [ \"$BUILD_MAC\" = true ]; then\n        # Determine target architecture (default to arm64)\n        local mac_target=\"aarch64-apple-darwin\"\n        local arch_name=\"Apple Silicon (arm64)\"\n\n        if [[ \" ${ARGS[*]} \" == *\" --x64 \"* ]]; then\n            mac_target=\"x86_64-apple-darwin\"\n            arch_name=\"Intel (x64)\"\n        fi\n\n        print_info \"Building macOS binaries for entire workspace ($arch_name)...\"\n        cargo build --release --workspace --target \"$mac_target\"\n\n        # Create symlinks for electron-builder compatibility\n        if [ \"$mac_target\" = \"aarch64-apple-darwin\" ]; then\n            print_info \"Creating symlink: arm64-apple-darwin -> aarch64-apple-darwin\"\n            ln -sfn aarch64-apple-darwin target/arm64-apple-darwin\n        else\n            print_info \"Creating symlink: x64-apple-darwin -> x86_64-apple-darwin\"\n            ln -sfn x86_64-apple-darwin target/x64-apple-darwin\n        fi\n\n        # Build Swift packages\n        print_info \"Building Swift packages...\"\n        cd cursor-context\n        if [ \"$mac_target\" = \"aarch64-apple-darwin\" ]; then\n            swift build -c release --arch arm64\n        else\n            swift build -c release --arch x86_64\n        fi\n        # Copy built binary to Rust target directory and re-sign for code signing compatibility\n        cp .build/release/cursor-context \"../target/$mac_target/release/\"\n        xattr -cr \"../target/$mac_target/release/cursor-context\"\n        codesign --force --sign - \"../target/$mac_target/release/cursor-context\" 2>/dev/null || true\n        cd ..\n    fi\n\n    # --- Windows Build ---\n    if [ \"$BUILD_WINDOWS\" = true ]; then\n        print_info \"Building Windows binary for entire workspace...\"\n\n        # Use MSVC on Windows (better AV compatibility), GNU for cross-compilation\n        if [ \"$compiling_on_windows\" = true ]; then\n            print_info \"Building with MSVC toolchain on Windows...\"\n            cargo build --release --target x86_64-pc-windows-msvc\n        else\n            # Cross-compile from macOS/Linux using GNU toolchain\n            print_info \"Cross-compiling with GNU toolchain...\"\n            cargo build --release --target x86_64-pc-windows-gnu\n        fi\n    fi\n\n    # Return to the project root\n    cd ..\n}\n\n\n# --- Main Script ---\n\nprint_status \"Starting native module build process...\"\n\n# Check if rustup is installed before doing anything else\nif ! command -v rustup &> /dev/null; then\n    print_error \"rustup is not installed. Please install it first: https://rustup.rs/\"\n    exit 1\nfi\n\n# Store all script arguments in an array\nARGS=(\"$@\")\n\n# Determine which platforms to build for\nBUILD_MAC=false\nif [[ \" ${ARGS[*]} \" == *\" --mac \"* ]] || [[ \" ${ARGS[*]} \" == *\" --all \"* ]]; then\n  BUILD_MAC=true\nfi\n\nBUILD_WINDOWS=false\nif [[ \" ${ARGS[*]} \" == *\" --windows \"* ]] || [[ \" ${ARGS[*]} \" == *\" --all \"* ]]; then\n  BUILD_WINDOWS=true\nfi\n\n# If no platform flags are provided, print usage and exit.\nif [ \"$BUILD_MAC\" = false ] && [ \"$BUILD_WINDOWS\" = false ]; then\n    print_error \"No platform specified. Use --mac, --windows, or --all.\"\n    echo \"Usage: $0 [--mac] [--windows] [--all] [--x64]\"\n    echo \"\"\n    echo \"Options:\"\n    echo \"  --mac       Build for macOS (defaults to arm64, use --x64 for Intel)\"\n    echo \"  --windows   Build for Windows\"\n    echo \"  --all       Build for all platforms\"\n    echo \"  --x64       Build for x64/Intel instead of arm64 (macOS only)\"\n    exit 1\nfi\n\n# Add required Rust targets\nif [ \"$BUILD_MAC\" = true ]; then\n    # Determine which macOS target to add\n    if [[ \" ${ARGS[*]} \" == *\" --x64 \"* ]]; then\n        print_status \"Adding macOS x64 target...\"\n        rustup target add x86_64-apple-darwin\n    else\n        print_status \"Adding macOS arm64 target...\"\n        rustup target add aarch64-apple-darwin\n    fi\nfi\nif [ \"$BUILD_WINDOWS\" = true ]; then\n    print_status \"Adding Windows target...\"\n\n    # Check if we're compiling on a Windows machine\n    compiling_on_windows=false\n    if [[ \"$OSTYPE\" == \"msys\" ]] || [[ \"$OSTYPE\" == \"win32\" ]] || [[ \"$OS\" == \"Windows_NT\" ]]; then\n        compiling_on_windows=true\n    fi\n\n    if [ \"$compiling_on_windows\" = true ]; then\n        # On Windows, use MSVC toolchain (better AV compatibility)\n        print_info \"Using MSVC toolchain on Windows for better antivirus compatibility\"\n        rustup target add x86_64-pc-windows-msvc\n    else\n        # For cross-compilation from macOS/Linux, use GNU toolchain\n        print_info \"Setting up GNU toolchain for cross-compilation\"\n        rustup target add x86_64-pc-windows-gnu\n\n        if [[ \"$OSTYPE\" == \"darwin\"* ]]; then\n            # On macOS, check if MinGW-w64 is installed\n            if command -v x86_64-w64-mingw32-gcc &> /dev/null; then\n                print_info \"Using MinGW-w64 cross-compiler for Windows builds on macOS\"\n            elif brew list mingw-w64 &> /dev/null; then\n                print_info \"MinGW-w64 found via Homebrew, using for Windows cross-compilation\"\n            else\n                print_error \"Windows GNU target requires MinGW-w64 toolchain. Install with: brew install mingw-w64\"\n                exit 1\n            fi\n        else\n            # On Linux, check if MinGW-w64 is installed\n            if command -v x86_64-w64-mingw32-gcc &> /dev/null; then\n                print_info \"Using MinGW-w64 cross-compiler for Windows builds on Linux\"\n            else\n                print_error \"Windows GNU target requires MinGW-w64 toolchain. Install with: sudo apt-get install mingw-w64\"\n                exit 1\n            fi\n        fi\n    fi\nfi\n\n\n# --- Build the native workspace ---\nbuild_native_workspace\n\nprint_status \"Native workspace build completed successfully!\""
  },
  {
    "path": "commitlint.config.js",
    "content": "// commitlint.config.js\nmodule.exports = {\n  extends: ['@commitlint/config-conventional'],\n}\n"
  },
  {
    "path": "components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": false,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"app/styles/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true\n  },\n  \"aliases\": {\n    \"components\": \"@/app/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/app/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/lib/hooks\"\n  },\n  \"iconLibrary\": \"lucide\"\n}\n"
  },
  {
    "path": "dev-app-update.yml",
    "content": "owner: heyito\nrepo: ito\nprovider: s3\nbucket: dev-ito-releases\npath: manual-test/\nregion: us-west-2\n"
  },
  {
    "path": "electron-builder.config.js",
    "content": "// Define the native binaries that are shared across platforms\nconst nativeBinaries = [\n  'global-key-listener',\n  'audio-recorder',\n  'text-writer',\n  'active-application',\n  'selected-text-reader',\n]\n\nconst getMacResources = () =>\n  nativeBinaries.map(binary => ({\n    from: `native/target/\\${arch}-apple-darwin/release/${binary}`,\n    to: `binaries/${binary}`,\n  }))\n\nconst getWindowsResources = () =>\n  nativeBinaries.map(binary => ({\n    from: `native/target/x86_64-pc-windows-msvc/release/${binary}.exe`,\n    to: `binaries/${binary}.exe`,\n  }))\n\nconst stage = process.env.ITO_ENV || 'prod'\nmodule.exports = {\n  appId: stage === 'prod' ? 'ai.ito.ito' : `ai.ito.ito-${stage.toLowerCase()}`,\n  productName: stage === 'prod' ? 'Ito' : `Ito-${stage}`,\n  copyright: 'Copyright © 2025 Demox Labs',\n  directories: {\n    buildResources: 'resources',\n    output: 'dist',\n  },\n  files: [\n    '!**/.vscode/*',\n    '!src/*',\n    '!electron.vite.config.{js,ts,mjs,cjs}',\n    '!.eslintignore',\n    '!.eslintrc.cjs',\n    '!.prettierignore',\n    '!.prettierrc.yaml',\n    '!README.md',\n    '!.env',\n    '!.env.*',\n    '!.npmrc',\n    '!pnpm-lock.yaml',\n    '!tsconfig.json',\n    '!tsconfig.node.json',\n    '!tsconfig.web.json',\n    '!native/**',\n    '!build-*.sh',\n    {\n      from: 'out',\n      filter: ['**/*'],\n    },\n  ],\n  asar: true,\n  asarUnpack: ['resources/**'],\n  extraMetadata: {\n    version: process.env.VITE_ITO_VERSION || '0.0.0-dev',\n  },\n  protocols: {\n    name: 'ito',\n    schemes: stage === 'prod' ? ['ito'] : [`ito-dev`],\n  },\n  mac: {\n    target: 'default',\n    icon: 'resources/build/icon.icns',\n    darkModeSupport: true,\n    hardenedRuntime: true,\n    gatekeeperAssess: false,\n    identity: 'Demox Labs, Inc. (294ZSTM7UB)',\n    notarize: true,\n    entitlements: 'build/entitlements.mac.plist',\n    entitlementsInherit: 'build/entitlements.mac.inherit.plist',\n    extendInfo: {\n      NSMicrophoneUsageDescription:\n        'Ito requires microphone access to transcribe your speech.',\n    },\n    extraResources: [\n      ...getMacResources(),\n      { from: 'resources/build/ito-logo.png', to: 'build/ito-logo.png' },\n    ],\n  },\n  dmg: {\n    artifactName:\n      stage === 'prod'\n        ? 'Ito-Installer.${ext}'\n        : `Ito-${stage}-Installer.\\${ext}`,\n  },\n  win: {\n    target: [\n      {\n        target: 'zip',\n        arch: ['x64'],\n      },\n      {\n        target: 'nsis',\n        arch: ['x64'],\n      },\n    ],\n    artifactName: '${productName}-${version}.${ext}',\n    icon: 'resources/build/icon.ico',\n    executableName: 'Ito',\n    requestedExecutionLevel: 'asInvoker',\n    extraResources: [\n      ...getWindowsResources(),\n      { from: 'resources/build/ito-logo.png', to: 'build/ito-logo.png' },\n    ],\n    forceCodeSigning: false,\n    asarUnpack: [\n      'resources/**',\n      '**/node_modules/@sentry/**',\n      '**/node_modules/sqlite3/**',\n    ],\n  },\n  nodeGypRebuild: false,\n  buildDependenciesFromSource: false,\n  nsis: {\n    shortcutName: '${productName}',\n    uninstallDisplayName: '${productName}-uninstaller',\n    createDesktopShortcut: false,\n    createStartMenuShortcut: true,\n    oneClick: false,\n    perMachine: false,\n    allowToChangeInstallationDirectory: false,\n    deleteAppDataOnUninstall: true,\n  },\n}\n"
  },
  {
    "path": "electron.vite.config.ts",
    "content": "import { sentryVitePlugin } from '@sentry/vite-plugin'\nimport { resolve } from 'path'\nimport react from '@vitejs/plugin-react'\nimport tailwindcss from '@tailwindcss/vite'\nimport { defineConfig, externalizeDepsPlugin } from 'electron-vite'\n\nexport default defineConfig({\n  main: {\n    build: {\n      sourcemap: true,\n      rollupOptions: {\n        input: {\n          main: resolve(__dirname, 'lib/main/main.ts'),\n        },\n      },\n    },\n    resolve: {\n      alias: {\n        '@/app': resolve(__dirname, 'app'),\n        '@/lib': resolve(__dirname, 'lib'),\n        '@/resources': resolve(__dirname, 'resources'),\n      },\n    },\n    plugins: [\n      externalizeDepsPlugin(),\n      sentryVitePlugin({ org: 'demox-labs', project: 'ito' }),\n    ],\n  },\n\n  preload: {\n    build: {\n      sourcemap: true,\n      rollupOptions: {\n        input: {\n          preload: resolve(__dirname, 'lib/preload/preload.ts'),\n        },\n      },\n    },\n    resolve: {\n      alias: {\n        '@/app': resolve(__dirname, 'app'),\n        '@/lib': resolve(__dirname, 'lib'),\n        '@/resources': resolve(__dirname, 'resources'),\n      },\n    },\n    plugins: [\n      externalizeDepsPlugin(),\n      sentryVitePlugin({ org: 'demox-labs', project: 'ito' }),\n    ],\n  },\n\n  renderer: {\n    root: './app',\n    build: {\n      sourcemap: true,\n      rollupOptions: {\n        input: {\n          index: resolve(__dirname, 'app/index.html'),\n        },\n      },\n    },\n    resolve: {\n      alias: {\n        '@/app': resolve(__dirname, 'app'),\n        '@/lib': resolve(__dirname, 'lib'),\n        '@/resources': resolve(__dirname, 'resources'),\n      },\n    },\n    plugins: [\n      tailwindcss(),\n      react(),\n      sentryVitePlugin({ org: 'demox-labs', project: 'ito' }),\n    ],\n  },\n})\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import eslint from '@eslint/js'\nimport tseslint from 'typescript-eslint'\nimport reactPlugin from 'eslint-plugin-react'\nimport reactHooksPlugin from 'eslint-plugin-react-hooks'\n\nexport default [\n  // Global ignores must be in their own config object\n  {\n    ignores: [\n      '**/node_modules',\n      '**/node_modules/**',\n      '.pnpm-store',\n      'dist',\n      'build',\n      'out',\n      '.vite',\n      'server/infra/cdk.out',\n      'resources/binaries',\n      '**/*.min.js',\n      '**/*.bundle.js',\n      '**/dist',\n      '**/generated',\n      '**/*.pb.ts',\n      '**/*_pb.ts',\n      '**/*_connect.ts',\n      'server/src/ito_*',\n      'server/src/migrations',\n      'scripts',\n      'native',\n      '**/target',\n      '*.config.js',\n      'commitlint.config.js',\n      'electron-builder.config.js',\n      'tailwind.config.js',\n      'shared-constants.js',\n      'server/infra/jest.config.js',\n      // CDK outputs\n      'server/infra/cdk.out/**',\n      'server/infra/**/*.d.ts',\n      'server/infra/**/*.js',\n    ],\n  },\n  eslint.configs.recommended,\n  ...tseslint.configs.recommended,\n  {\n    files: ['**/*.{js,jsx,ts,tsx}'],\n    plugins: {\n      react: reactPlugin,\n      'react-hooks': reactHooksPlugin,\n    },\n    languageOptions: {\n      ecmaVersion: 'latest',\n      sourceType: 'module',\n      parser: tseslint.parser,\n      parserOptions: {\n        ecmaFeatures: { jsx: true },\n        projectService: true,\n      },\n      globals: {\n        // Browser globals that should be readonly\n        window: 'readonly',\n        document: 'readonly',\n        location: 'readonly',\n        history: 'readonly',\n        navigator: 'readonly',\n\n        // Browser globals that can be modified\n        console: 'writable',\n        localStorage: 'writable',\n        sessionStorage: 'writable',\n\n        // Timer functions that can be modified\n        setTimeout: 'writable',\n        clearTimeout: 'writable',\n        setInterval: 'writable',\n        clearInterval: 'writable',\n\n        // Node.js globals\n        process: 'readonly',\n        __dirname: 'readonly',\n        __filename: 'readonly',\n\n        // React globals\n        React: 'readonly',\n      },\n    },\n    settings: {\n      react: {\n        version: 'detect',\n      },\n    },\n    rules: {\n      // React specific rules\n      'react/react-in-jsx-scope': 'off',\n      'react-hooks/rules-of-hooks': 'error',\n      'react-hooks/exhaustive-deps': 'warn',\n\n      // TypeScript specific rules\n      '@typescript-eslint/no-unused-vars': [\n        'warn',\n        {\n          argsIgnorePattern: '^_',\n          varsIgnorePattern: '^_',\n        },\n      ],\n\n      // General rules\n      'no-console': ['warn', { allow: ['warn', 'error', 'info', 'log'] }],\n      '@typescript-eslint/no-explicit-any': 'off',\n\n      // Global modification rules\n      'no-global-assign': [\n        'error',\n        {\n          exceptions: ['console', 'localStorage', 'sessionStorage'],\n        },\n      ],\n    },\n  },\n  // Add specific configuration for preload files\n  {\n    files: ['app/**/*.ts', 'lib/**/*.ts', 'app/**/*.tsx', 'lib/**/*.tsx'],\n    languageOptions: {\n      globals: {\n        process: 'readonly',\n        console: 'readonly',\n        window: 'readonly',\n      },\n    },\n  },\n  // Test file specific configuration\n  {\n    files: ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx'],\n    rules: {\n      '@typescript-eslint/no-require-imports': 'off',\n      'no-console': 'off',\n    },\n  },\n]\n"
  },
  {
    "path": "lib/__tests__/fixtures/auth.ts",
    "content": "// JWT token fixtures for testing\nexport const VALID_JWT_TOKEN =\n  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0LXVzZXItMTIzIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmFtZSI6IlRlc3QgVXNlciIsInBpY3R1cmUiOiJodHRwczovL2V4YW1wbGUuY29tL2F2YXRhci5qcGciLCJpYXQiOjE3MzUzNDk4ODAsImV4cCI6OTk5OTk5OTk5OX0.fake-signature'\n\nexport const EXPIRED_JWT_TOKEN =\n  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0LXVzZXItMTIzIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmFtZSI6IlRlc3QgVXNlciIsInBpY3R1cmUiOiJodHRwczovL2V4YW1wbGUuY29tL2F2YXRhci5qcGciLCJpYXQiOjE3MzUzNDk4ODAsImV4cCI6MTczNTM0OTg4MH0.fake-signature'\n\nexport const MALFORMED_JWT_TOKEN = 'invalid.jwt.token'\n\n// Auth0 configuration fixtures\nexport const VALID_AUTH0_CONFIG = {\n  domain: 'test-domain.auth0.com',\n  clientId: 'test-client-id',\n  audience: 'https://api.test.com',\n  redirectUri: 'http://localhost:3000/callback',\n  scope: 'openid profile email offline_access',\n  useRefreshTokens: true,\n  cacheLocation: 'localstorage' as const,\n}\n\nexport const INCOMPLETE_AUTH0_CONFIG = {\n  domain: 'test-domain.auth0.com',\n  clientId: '', // Missing client ID\n  audience: 'https://api.test.com',\n  redirectUri: 'http://localhost:3000/callback',\n}\n\n// Token response fixtures\nexport const VALID_TOKEN_RESPONSE = {\n  access_token: VALID_JWT_TOKEN,\n  id_token: VALID_JWT_TOKEN,\n  refresh_token: 'refresh-token-123',\n  token_type: 'Bearer',\n  expires_in: 86400,\n}\n\nexport const REFRESH_TOKEN_RESPONSE = {\n  access_token: VALID_JWT_TOKEN,\n  id_token: VALID_JWT_TOKEN,\n  token_type: 'Bearer',\n  expires_in: 86400,\n  // Note: refresh_token may or may not be included in refresh response\n}\n\nexport const TOKEN_ERROR_RESPONSE = {\n  error: 'invalid_grant',\n  error_description: 'The refresh token is invalid',\n}\n\n// User profile fixtures\nexport const SAMPLE_USER_PROFILE = {\n  id: 'test-user-123',\n  email: 'test@example.com',\n  name: 'Test User',\n  picture: 'https://example.com/avatar.jpg',\n  email_verified: true,\n  updated_at: '2024-01-01T00:00:00.000Z',\n}\n\nexport const SAMPLE_USER_PROFILE_MINIMAL = {\n  id: 'test-user-456',\n  email: 'minimal@example.com',\n}\n\n// Auth state fixtures\nexport const SAMPLE_AUTH_STATE = {\n  id: 'test-auth-state-id-123',\n  state: 'random-state-string-123',\n  codeVerifier: 'code-verifier-123',\n  codeChallenge: 'code-challenge-123',\n}\n\nexport const SAMPLE_STORED_AUTH = {\n  isAuthenticated: true,\n  tokens: {\n    access_token: VALID_JWT_TOKEN,\n    id_token: VALID_JWT_TOKEN,\n    refresh_token: 'refresh-token-123',\n    expires_at: Date.now() + 86400000, // 24 hours from now\n  },\n  state: SAMPLE_AUTH_STATE,\n  userProfile: SAMPLE_USER_PROFILE,\n}\n\nexport const SAMPLE_EXPIRED_AUTH = {\n  isAuthenticated: true,\n  tokens: {\n    access_token: EXPIRED_JWT_TOKEN,\n    id_token: EXPIRED_JWT_TOKEN,\n    refresh_token: 'refresh-token-123',\n    expires_at: Date.now() - 3600000, // 1 hour ago\n  },\n  state: SAMPLE_AUTH_STATE,\n  userProfile: SAMPLE_USER_PROFILE,\n}\n\n// Network response mocks\nexport const createSuccessfulTokenResponse = (overrides = {}) => ({\n  ok: true,\n  status: 200,\n  statusText: 'OK',\n  json: async () => ({ ...VALID_TOKEN_RESPONSE, ...overrides }),\n})\n\nexport const createFailedTokenResponse = (\n  status = 400,\n  error = TOKEN_ERROR_RESPONSE,\n) => ({\n  ok: false,\n  status,\n  statusText: status === 400 ? 'Bad Request' : 'Internal Server Error',\n  text: async () => JSON.stringify(error),\n})\n\n// Environment variable mocks for Auth0 config\nexport const VALID_ENV_VARS = {\n  VITE_AUTH0_DOMAIN: 'test-domain.auth0.com',\n  VITE_AUTH0_CLIENT_ID: 'test-client-id',\n  VITE_AUTH0_AUDIENCE: 'https://api.test.com',\n}\n\nexport const INCOMPLETE_ENV_VARS = {\n  VITE_AUTH0_DOMAIN: 'test-domain.auth0.com',\n  VITE_AUTH0_CLIENT_ID: '', // Missing\n  VITE_AUTH0_AUDIENCE: 'https://api.test.com',\n}\n\n// Helper functions\nexport const createTokensWithExpiry = (expiresInMinutes: number) => ({\n  ...VALID_TOKEN_RESPONSE,\n  expires_at: Date.now() + expiresInMinutes * 60 * 1000,\n})\n\nexport const createJWTWithExpiry = (expiresInSeconds: number) => {\n  const payload = {\n    sub: 'test-user-123',\n    email: 'test@example.com',\n    name: 'Test User',\n    iat: Math.floor(Date.now() / 1000),\n    exp: Math.floor(Date.now() / 1000) + expiresInSeconds,\n  }\n\n  // Simple base64 encoding for testing (not a real JWT signature)\n  const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))\n  const payloadEncoded = btoa(JSON.stringify(payload))\n  return `${header}.${payloadEncoded}.fake-signature`\n}\n"
  },
  {
    "path": "lib/__tests__/fixtures/database.ts",
    "content": "import type {\n  Interaction,\n  Note,\n  DictionaryItem,\n} from '../../main/sqlite/models'\n\n// Test user IDs\nexport const TEST_USER_ID = 'test-user-123'\nexport const TEST_USER_ID_2 = 'test-user-456'\n\n// Sample interaction data\nexport const sampleInteraction: Interaction = {\n  id: 'interaction-1',\n  user_id: TEST_USER_ID,\n  title: 'Sample Interaction',\n  asr_output: {\n    transcript: 'Hello world',\n    words: [\n      { text: 'Hello', start: 0, end: 1 },\n      { text: 'world', start: 1, end: 2 },\n    ],\n  },\n  llm_output: {\n    response: 'Hello to you too!',\n    confidence: 0.95,\n  },\n  raw_audio: Buffer.from('fake-audio-data'),\n  duration_ms: 2000,\n  created_at: '2024-01-01T10:00:00.000Z',\n  updated_at: '2024-01-01T10:00:00.000Z',\n  deleted_at: null,\n}\n\nexport const sampleInteractionNoUser: Interaction = {\n  id: 'interaction-2',\n  user_id: null,\n  title: 'Anonymous Interaction',\n  asr_output: {\n    transcript: 'Anonymous test',\n  },\n  llm_output: {\n    response: 'Anonymous response',\n  },\n  raw_audio: null,\n  duration_ms: 1500,\n  created_at: '2024-01-01T11:00:00.000Z',\n  updated_at: '2024-01-01T11:00:00.000Z',\n  deleted_at: null,\n}\n\nexport const sampleDeletedInteraction: Interaction = {\n  id: 'interaction-3',\n  user_id: TEST_USER_ID,\n  title: 'Deleted Interaction',\n  asr_output: { transcript: 'Deleted content' },\n  llm_output: { response: 'Deleted response' },\n  raw_audio: null,\n  duration_ms: 1000,\n  created_at: '2024-01-01T09:00:00.000Z',\n  updated_at: '2024-01-01T12:00:00.000Z',\n  deleted_at: '2024-01-01T12:00:00.000Z',\n}\n\n// Sample note data\nexport const sampleNote: Note = {\n  id: 'note-1',\n  user_id: TEST_USER_ID,\n  interaction_id: 'interaction-1',\n  content: 'This is a sample note content',\n  created_at: '2024-01-01T10:00:00.000Z',\n  updated_at: '2024-01-01T10:00:00.000Z',\n  deleted_at: null,\n}\n\nexport const sampleNoteNoInteraction: Note = {\n  id: 'note-2',\n  user_id: TEST_USER_ID,\n  interaction_id: null,\n  content: 'Standalone note without interaction',\n  created_at: '2024-01-01T11:00:00.000Z',\n  updated_at: '2024-01-01T11:00:00.000Z',\n  deleted_at: null,\n}\n\nexport const sampleDeletedNote: Note = {\n  id: 'note-3',\n  user_id: TEST_USER_ID,\n  interaction_id: 'interaction-1',\n  content: 'Deleted note content',\n  created_at: '2024-01-01T09:00:00.000Z',\n  updated_at: '2024-01-01T12:00:00.000Z',\n  deleted_at: '2024-01-01T12:00:00.000Z',\n}\n\n// Sample dictionary data\nexport const sampleDictionaryItem: DictionaryItem = {\n  id: 'dict-1',\n  user_id: TEST_USER_ID,\n  word: 'pronunciation',\n  pronunciation: 'pruh-nuhn-see-AY-shuhn',\n  created_at: '2024-01-01T10:00:00.000Z',\n  updated_at: '2024-01-01T10:00:00.000Z',\n  deleted_at: null,\n}\n\nexport const sampleDictionaryItemNoPronunciation: DictionaryItem = {\n  id: 'dict-2',\n  user_id: TEST_USER_ID,\n  word: 'simple',\n  pronunciation: null,\n  created_at: '2024-01-01T11:00:00.000Z',\n  updated_at: '2024-01-01T11:00:00.000Z',\n  deleted_at: null,\n}\n\nexport const sampleDeletedDictionaryItem: DictionaryItem = {\n  id: 'dict-3',\n  user_id: TEST_USER_ID,\n  word: 'deleted',\n  pronunciation: 'dih-LEET-ed',\n  created_at: '2024-01-01T09:00:00.000Z',\n  updated_at: '2024-01-01T12:00:00.000Z',\n  deleted_at: '2024-01-01T12:00:00.000Z',\n}\n\n// Collections for easy testing\nexport const allSampleInteractions = [\n  sampleInteraction,\n  sampleInteractionNoUser,\n  sampleDeletedInteraction,\n]\n\nexport const allSampleNotes = [\n  sampleNote,\n  sampleNoteNoInteraction,\n  sampleDeletedNote,\n]\n\nexport const allSampleDictionaryItems = [\n  sampleDictionaryItem,\n  sampleDictionaryItemNoPronunciation,\n  sampleDeletedDictionaryItem,\n]\n\n// Helper functions for creating variations\nexport const createInteractionForUser = (\n  userId: string,\n  overrides: Partial<Interaction> = {},\n): Interaction => ({\n  ...sampleInteraction,\n  id: `interaction-${Date.now()}-${Math.random()}`,\n  user_id: userId,\n  ...overrides,\n})\n\nexport const createNoteForUser = (\n  userId: string,\n  overrides: Partial<Note> = {},\n): Note => ({\n  ...sampleNote,\n  id: `note-${Date.now()}-${Math.random()}`,\n  user_id: userId,\n  ...overrides,\n})\n\nexport const createDictionaryItemForUser = (\n  userId: string,\n  overrides: Partial<DictionaryItem> = {},\n): DictionaryItem => ({\n  ...sampleDictionaryItem,\n  id: `dict-${Date.now()}-${Math.random()}`,\n  user_id: userId,\n  ...overrides,\n})\n\n// Migration test data\nexport const sampleMigrations = [\n  {\n    id: '0000_initial_schema',\n    applied_at: '2024-01-01T00:00:00.000Z',\n  },\n  {\n    id: '20250108120000_add_raw_audio_to_interactions',\n    applied_at: '2024-01-08T12:00:00.000Z',\n  },\n  {\n    id: '20250108130000_add_duration_to_interactions',\n    applied_at: '2024-01-08T13:00:00.000Z',\n  },\n]\n"
  },
  {
    "path": "lib/__tests__/helpers/testUtils.ts",
    "content": "import { mock } from 'bun:test'\nimport { install } from '@sinonjs/fake-timers'\nimport { afterAll, beforeEach } from 'bun:test'\n\nexport function fakeTimers() {\n  const clock = install()\n\n  beforeEach(() => {\n    clock.reset()\n  })\n\n  afterAll(() => {\n    clock.uninstall()\n  })\n\n  return clock\n}\n\n// Helper to create a mock module with all exports\nexport const createMockModule = (moduleExports: Record<string, any>) => {\n  return mock(() => moduleExports)\n}\n\n// Helper to reset all mocks in an object\nexport const resetAllMocks = (mockObject: Record<string, any>) => {\n  Object.values(mockObject).forEach(mockFn => {\n    if (typeof mockFn === 'function' && mockFn.mockClear) {\n      mockFn.mockClear()\n    }\n  })\n}\n\n// Helper to create a promise that resolves/rejects after a delay\nexport const createDelayedPromise = <T>(\n  value: T,\n  delay: number = 0,\n  shouldReject: boolean = false,\n): Promise<T> => {\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      if (shouldReject) {\n        reject(value)\n      } else {\n        resolve(value)\n      }\n    }, delay)\n  })\n}\n\n// Helper to create a mock function that tracks call order\nexport const createOrderedMock = () => {\n  const calls: number[] = []\n  let callCount = 0\n\n  const mockFn = mock(() => {\n    calls.push(++callCount)\n    return callCount\n  })\n\n  return {\n    mock: mockFn,\n    calls,\n    getCallOrder: () => calls,\n    reset: () => {\n      calls.length = 0\n      callCount = 0\n      mockFn.mockClear()\n    },\n  }\n}\n\n// Helper to suppress console output during tests\nexport const suppressConsole = (\n  methods: ('log' | 'warn' | 'error' | 'info')[] = [\n    'log',\n    'warn',\n    'error',\n    'info',\n  ],\n) => {\n  const originalMethods: Record<string, any> = {}\n\n  const suppress = () => {\n    methods.forEach(method => {\n      // eslint-disable-next-line no-console\n      originalMethods[method] = console[method]\n      // eslint-disable-next-line no-console\n      console[method] = mock()\n    })\n  }\n\n  const restore = () => {\n    methods.forEach(method => {\n      if (originalMethods[method]) {\n        // eslint-disable-next-line no-console\n        console[method] = originalMethods[method]\n      }\n    })\n  }\n\n  return { suppress, restore }\n}\n\n// Helper for testing async functions with timeouts\nexport const withTimeout = <T>(\n  promise: Promise<T>,\n  timeoutMs: number = 5000,\n  timeoutMessage: string = 'Operation timed out',\n): Promise<T> => {\n  return Promise.race([\n    promise,\n    new Promise<T>((_, reject) => {\n      setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs)\n    }),\n  ])\n}\n\n// Helper to create a mock that throws on specific call numbers\nexport const createThrowingMock = (throwOnCalls: number[] = []) => {\n  let callCount = 0\n\n  return mock(() => {\n    callCount++\n    if (throwOnCalls.includes(callCount)) {\n      throw new Error(`Mock error on call ${callCount}`)\n    }\n    return `call-${callCount}`\n  })\n}\n\n// Helper to create a spy that tracks arguments\nexport const createArgSpy = <T extends any[]>() => {\n  const capturedArgs: T[] = []\n\n  const spy = mock((...args: T) => {\n    capturedArgs.push(args)\n    return args\n  })\n\n  return {\n    spy,\n    capturedArgs,\n    getArgs: (callIndex: number) => capturedArgs[callIndex],\n    getAllArgs: () => capturedArgs,\n    reset: () => {\n      capturedArgs.length = 0\n      spy.mockClear()\n    },\n  }\n}\n"
  },
  {
    "path": "lib/__tests__/mocks/electron.ts",
    "content": "import { mock } from 'bun:test'\n\n// Mock electron app\nexport const mockApp = mock(() => ({\n  getPath: mock((name: string) => {\n    const paths: Record<string, string> = {\n      userData: '/tmp/test-ito-app',\n      documents: '/tmp/test-documents',\n      desktop: '/tmp/test-desktop',\n    }\n    return paths[name] || '/tmp/test-default'\n  }),\n  quit: mock(() => {}),\n}))\n\n// Mock BrowserWindow\nexport const mockBrowserWindow = mock(() => ({\n  webContents: {\n    send: mock((_channel: string, ..._args: any[]) => {}),\n  },\n  isDestroyed: mock(() => false),\n  loadURL: mock((_url: string) => Promise.resolve()),\n  close: mock(() => {}),\n}))\n\n// Mock IPC Main\nexport const mockIpcMain = mock(() => ({\n  handle: mock((_channel: string, _handler: (...args: any[]) => any) => {}),\n  on: mock((_channel: string, _handler: (...args: any[]) => any) => {}),\n  removeAllListeners: mock((_channel?: string) => {}),\n}))\n\n// Mock IPC Renderer\nexport const mockIpcRenderer = mock(() => ({\n  invoke: mock((_channel: string, ..._args: any[]) => Promise.resolve()),\n  send: mock((_channel: string, ..._args: any[]) => {}),\n  on: mock((_channel: string, _handler: (...args: any[]) => any) => {}),\n  removeAllListeners: mock((_channel?: string) => {}),\n}))\n\n// Mock context bridge\nexport const mockContextBridge = mock(() => ({\n  exposeInMainWorld: mock((_apiKey: string, _api: any) => {}),\n}))\n\n// Mock electron store\nexport const mockElectronStore = mock(() => {\n  const store = new Map()\n\n  return {\n    get: mock(\n      (key: string, defaultValue?: any) => store.get(key) ?? defaultValue,\n    ),\n    set: mock((key: string, value: any) => store.set(key, value)),\n    delete: mock((key: string) => store.delete(key)),\n    clear: mock(() => store.clear()),\n    has: mock((key: string) => store.has(key)),\n    size: store.size,\n    store: Object.fromEntries(store),\n  }\n})\n\n// Reset function for clearing all mocks\nexport const resetElectronMocks = () => {\n  mockApp.mockClear()\n  mockBrowserWindow.mockClear()\n  mockIpcMain.mockClear()\n  mockIpcRenderer.mockClear()\n  mockContextBridge.mockClear()\n  mockElectronStore.mockClear()\n}\n"
  },
  {
    "path": "lib/__tests__/mocks/sqlite.ts",
    "content": "import { mock } from 'bun:test'\n\n// Mock SQLite database\nexport const mockSqliteDatabase = mock(() => {\n  const db = new Map<string, any[]>()\n\n  return {\n    run: mock(\n      (\n        _query: string,\n        _params: any[] = [],\n        callback?: (err: Error | null) => void,\n      ) => {\n        // Simulate successful execution\n        if (callback) callback(null)\n      },\n    ),\n\n    get: mock(\n      (\n        query: string,\n        params: any[] = [],\n        callback?: (err: Error | null, row?: any) => void,\n      ) => {\n        // Return mock data based on query\n        const mockRow = getMockRowForQuery(query, params)\n        if (callback) callback(null, mockRow)\n      },\n    ),\n\n    all: mock(\n      (\n        query: string,\n        params: any[] = [],\n        callback?: (err: Error | null, rows?: any[]) => void,\n      ) => {\n        // Return mock data based on query\n        const mockRows = getMockRowsForQuery(query, params)\n        if (callback) callback(null, mockRows)\n      },\n    ),\n\n    exec: mock((_query: string, callback?: (err: Error | null) => void) => {\n      // Simulate successful execution\n      if (callback) callback(null)\n    }),\n\n    close: mock((callback?: (err: Error | null) => void) => {\n      if (callback) callback(null)\n    }),\n\n    // Internal storage for testing\n    _testData: db,\n    _addTestData: (table: string, rows: any[]) => db.set(table, rows),\n    _clearTestData: () => db.clear(),\n  }\n})\n\n// Helper to generate mock rows based on query patterns\nconst getMockRowForQuery = (query: string, _params: any[] = []) => {\n  if (query.includes('SELECT * FROM migrations')) {\n    return { id: '0000_initial_schema', applied_at: '2024-01-01T00:00:00.000Z' }\n  }\n\n  if (query.includes('SELECT * FROM interactions')) {\n    return {\n      id: 'test-interaction-id',\n      user_id: 'test-user-id',\n      title: 'Test Interaction',\n      asr_output: '{\"transcript\": \"hello world\"}',\n      llm_output: '{\"response\": \"Hello!\"}',\n      raw_audio: null,\n      duration_ms: 1000,\n      created_at: '2024-01-01T00:00:00.000Z',\n      updated_at: '2024-01-01T00:00:00.000Z',\n      deleted_at: null,\n    }\n  }\n\n  if (query.includes('SELECT * FROM notes')) {\n    return {\n      id: 'test-note-id',\n      user_id: 'test-user-id',\n      interaction_id: 'test-interaction-id',\n      content: 'Test note content',\n      created_at: '2024-01-01T00:00:00.000Z',\n      updated_at: '2024-01-01T00:00:00.000Z',\n      deleted_at: null,\n    }\n  }\n\n  if (query.includes('SELECT * FROM dictionary_items')) {\n    return {\n      id: 'test-dict-id',\n      user_id: 'test-user-id',\n      word: 'test',\n      pronunciation: 'test',\n      created_at: '2024-01-01T00:00:00.000Z',\n      updated_at: '2024-01-01T00:00:00.000Z',\n      deleted_at: null,\n    }\n  }\n\n  if (query.includes('SELECT value FROM key_value_store')) {\n    return { value: 'test-value' }\n  }\n\n  return null\n}\n\nconst getMockRowsForQuery = (query: string, params: any[] = []) => {\n  const singleRow = getMockRowForQuery(query, params)\n  return singleRow ? [singleRow] : []\n}\n\n// Reset function\nexport const resetSqliteMocks = () => {\n  mockSqliteDatabase.mockClear()\n}\n"
  },
  {
    "path": "lib/__tests__/setup.ts",
    "content": "import { mock, afterEach, beforeEach, beforeAll } from 'bun:test'\nimport { promises as fs } from 'fs'\n\n// Simple, direct electron mock following Bun documentation pattern\nmock.module('electron', () => {\n  let userDataPath = '/tmp/test-ito-app'\n  let appName = 'Ito'\n  return {\n    app: {\n      getPath: (type: string) => {\n        if (type === 'userData') return userDataPath\n        return '/tmp/test-path'\n      },\n      setPath: (type: string, newPath: string) => {\n        if (type === 'userData') userDataPath = newPath\n      },\n      quit: () => {},\n      on: () => {},\n      getName: () => appName,\n      setName: (name: string) => {\n        appName = name\n      },\n      getVersion: () => '1.0.0',\n      whenReady: () => Promise.resolve(),\n      isReady: () => true,\n      isPackaged: false,\n      dock: {\n        hide: () => {},\n        show: () => {},\n      },\n    },\n    BrowserWindow: class MockBrowserWindow {\n      webContents: any\n\n      constructor() {\n        this.webContents = {\n          send: () => {},\n          on: () => {},\n          openDevTools: () => {},\n        }\n      }\n      loadURL() {}\n      loadFile() {}\n      on() {}\n      once() {}\n      show() {}\n      hide() {}\n      close() {}\n      destroy() {}\n      minimize() {}\n      maximize() {}\n      restore() {}\n      focus() {}\n      blur() {}\n      isFocused() {\n        return true\n      }\n      isVisible() {\n        return true\n      }\n      isMinimized() {\n        return false\n      }\n      isMaximized() {\n        return false\n      }\n      setTitle() {}\n      getTitle() {\n        return 'Test Window'\n      }\n    },\n    shell: {\n      openExternal: () => {},\n      showItemInFolder: () => {},\n      openPath: () => {},\n    },\n    screen: {\n      getPrimaryDisplay: () => ({\n        workAreaSize: { width: 1920, height: 1080 },\n        size: { width: 1920, height: 1080 },\n      }),\n      getAllDisplays: () => [],\n      getCursorScreenPoint: () => ({ x: 0, y: 0 }),\n    },\n    protocol: {\n      registerSchemesAsPrivileged: () => {},\n      registerFileProtocol: () => {},\n      registerHttpProtocol: () => {},\n      registerBufferProtocol: () => {},\n      registerStringProtocol: () => {},\n      unregisterProtocol: () => {},\n    },\n    net: {\n      request: () => {},\n    },\n    ipcMain: {\n      on: () => {},\n      once: () => {},\n      handle: () => {},\n      handleOnce: () => {},\n      removeAllListeners: () => {},\n      removeHandler: () => {},\n    },\n    ipcRenderer: {\n      invoke: () => {},\n      send: () => {},\n      on: () => {},\n      once: () => {},\n      removeAllListeners: () => {},\n      removeListener: () => {},\n      sendSync: (channel: string) => {\n        if (channel === 'electron-store-get-data') {\n          return {\n            encryptionKey: null,\n            migrations: {},\n            projectVersion: '1.0.0',\n            projectSuffix: 'test',\n            defaults: {},\n            name: 'config',\n            builtinMigrations: false,\n            clearInvalidConfig: false,\n            serialize: null,\n            deserialize: null,\n            appVersion: '1.0.0',\n            path: '/tmp/test-config.json',\n          }\n        }\n        return null\n      },\n    },\n    contextBridge: {\n      exposeInMainWorld: () => {},\n    },\n    systemPreferences: {\n      askForMediaAccess: () => {},\n      getMediaAccessStatus: () => 'granted',\n      getAnimationSettings: () => ({ shouldRenderRichAnimation: true }),\n    },\n    powerSaveBlocker: {\n      start: () => 1,\n      stop: () => {},\n      isStarted: () => false,\n    },\n    Menu: class MockMenu {},\n    MenuItem: class MockMenuItem {},\n    Tray: class MockTray {},\n    Notification: class MockNotification {},\n    dialog: {\n      showOpenDialog: () => {},\n      showSaveDialog: () => {},\n      showMessageBox: () => {},\n      showErrorBox: () => {},\n    },\n    clipboard: {\n      writeText: () => {},\n      readText: () => '',\n    },\n    nativeTheme: {\n      shouldUseDarkColors: false,\n      on: () => {},\n    },\n    IpcRendererEvent: class MockIpcRendererEvent {},\n    IpcMainEvent: class MockIpcMainEvent {},\n    autoUpdater: {\n      quitAndInstall: () => {},\n    },\n    powerMonitor: {\n      on: () => {},\n      getSystemIdleState: () => 'active',\n      getSystemIdleTime: () => 0,\n    },\n    crashReporter: {\n      start: () => {},\n      getLastCrashReport: () => null,\n      getUploadedReports: () => [],\n      getUploadToServer: () => true,\n      setUploadToServer: () => {},\n    },\n    nativeImage: {\n      createEmpty: () => ({}),\n      createFromPath: () => ({}),\n      createFromBuffer: () => ({}),\n      createFromDataURL: () => ({}),\n    },\n  }\n})\n\nconsole.log('✓ Electron module mocked')\n\n// Export a reusable mock TimingCollector factory for tests\n// Note: This is NOT globally mocked - tests must mock it themselves\nexport const createMockTimingCollector = () => ({\n  startInteraction: mock(),\n  startTiming: mock(),\n  endTiming: mock(),\n  finalizeInteraction: mock(),\n  clearInteraction: mock(),\n  timeAsync: mock(async (_eventName: any, fn: any, _interactionId?: any) => {\n    // Execute the function parameter\n    return await fn()\n  }),\n})\n// Ensure node:path join maps to path.join when tests mock path\nconst pathMod = await import('path')\nmock.module('node:path', () => ({ join: (pathMod as any).join }))\n\n// Initialize SQLite once for tests that touch the KeyValueStore\nbeforeAll(async () => {\n  // Ensure test userData directory exists for SQLite file\n  await fs.mkdir('/tmp/test-ito-app', { recursive: true })\n  try {\n    // Import after mocking electron so the mock is applied\n    const { initializeDatabase } = await import('../main/sqlite/db')\n    await initializeDatabase()\n  } catch (e) {\n    console.log(\n      '✓ Skipping DB init in this test run:',\n      (e as any)?.message || e,\n    )\n  }\n})\n\n// Store original console methods for restoration\nconst originalConsole = {\n  log: console.log,\n  error: console.error,\n  warn: console.warn,\n  info: console.info,\n}\n\n// Export function to restore console if needed in specific tests\n;(global as any).restoreConsole = () => {\n  Object.assign(console, originalConsole)\n}\n\n// Global setup: suppress console during test execution\nbeforeEach(() => {\n  // Suppress implementation console output during each test\n  global.console = {\n    ...console,\n    log: () => {}, // Suppress implementation logs\n    error: () => {}, // Suppress implementation errors\n    warn: () => {}, // Suppress implementation warnings\n    info: () => {}, // Suppress implementation info\n  }\n})\n\n// Global mock cleanup after each test\nafterEach(() => {\n  // Restore console first\n  Object.assign(console, originalConsole)\n\n  // Clear all mock function calls and implementations\n  mock.restore()\n})\n"
  },
  {
    "path": "lib/auth/config.test.ts",
    "content": "import { describe, test, expect } from 'bun:test'\nimport { Auth0Connections, RequiredAuth0Fields } from './config'\n\ndescribe('Auth0 Configuration', () => {\n  describe('required fields', () => {\n    test('documents required Auth0 environment variables', () => {\n      expect(RequiredAuth0Fields).toHaveLength(4)\n      expect(RequiredAuth0Fields).toContain('domain')\n      expect(RequiredAuth0Fields).toContain('clientId')\n      expect(RequiredAuth0Fields).toContain('redirectUri')\n      expect(RequiredAuth0Fields).toContain('audience')\n    })\n  })\n\n  describe('Auth0Connections', () => {\n    test('provides correct social provider connection identifiers', () => {\n      // These mappings are business-critical - Auth0 expects these exact strings\n      expect(Auth0Connections.google).toBe('google-oauth2')\n      expect(Auth0Connections.microsoft).toBe('windowslive')\n      expect(Auth0Connections.apple).toBe('apple')\n      expect(Auth0Connections.github).toBe('github')\n    })\n  })\n})\n"
  },
  {
    "path": "lib/auth/config.ts",
    "content": "// Auth0 configuration\nexport const Auth0Config = {\n  domain: import.meta.env.VITE_AUTH0_DOMAIN,\n  clientId: import.meta.env.VITE_AUTH0_CLIENT_ID,\n  audience: import.meta.env.VITE_AUTH0_AUDIENCE,\n  redirectUri: `${import.meta.env.VITE_GRPC_BASE_URL}/callback`,\n  scope: 'openid profile email offline_access',\n  useRefreshTokens: true,\n  cacheLocation: 'localstorage' as const,\n}\n\n// Social connection mappings\nexport const Auth0Connections = {\n  google: 'google-oauth2',\n  microsoft: 'windowslive',\n  apple: 'apple',\n  github: 'github',\n  database: 'ito-email-password',\n}\n\nexport const RequiredAuth0Fields = [\n  'domain',\n  'clientId',\n  'redirectUri',\n  'audience',\n]\n\n// Validate configuration\nexport const validateAuth0Config = () => {\n  const missing = RequiredAuth0Fields.filter(\n    key => !Auth0Config[key as keyof typeof Auth0Config],\n  )\n\n  if (missing.length > 0) {\n    throw new Error(`Missing Auth0 configuration: ${missing.join(', ')}`)\n  }\n\n  return true\n}\n"
  },
  {
    "path": "lib/auth/events.test.ts",
    "content": "import { describe, test, expect, mock, beforeEach } from 'bun:test'\nimport {\n  shouldRefreshToken,\n  isTokenExpired,\n  exchangeAuthCode,\n  handleLogin,\n  handleLogout,\n  refreshTokens,\n  ensureValidTokens,\n} from './events'\n\n// Mock jwt-decode for testing token expiration logic\nconst mockJwtDecode = mock()\nmock.module('jwt-decode', () => ({\n  jwtDecode: mockJwtDecode,\n}))\n\n// Mock store - used by both exchangeAuthCode and handleLogin\nconst mockStore = {\n  get: mock(),\n  set: mock(),\n  delete: mock(),\n}\nmock.module('../main/store', () => ({\n  default: mockStore,\n}))\n\n// Mock gRPC client\nconst mockGrpcClient = {\n  setAuthToken: mock(),\n}\nmock.module('../clients/grpcClient', () => ({\n  grpcClient: mockGrpcClient,\n}))\n\n// Mock sync service\nconst mockSyncService = {\n  start: mock(),\n  stop: mock(),\n}\nmock.module('../main/syncService', () => ({\n  syncService: mockSyncService,\n}))\n\n// Mock main window for notifications\nconst mockMainWindow = {\n  isDestroyed: mock().mockReturnValue(false),\n  webContents: {\n    send: mock(),\n  },\n}\nmock.module('../main/app', () => ({\n  mainWindow: mockMainWindow,\n}))\n\n// Mock store keys\nmock.module('../constants/store-keys', () => ({\n  STORE_KEYS: {\n    USER_PROFILE: 'userProfile',\n    ID_TOKEN: 'idToken',\n    ACCESS_TOKEN: 'accessToken',\n    AUTH: 'auth',\n  },\n}))\n\n// Mock fetch for network calls\nconst mockFetch = mock()\nglobal.fetch = mockFetch as any\n\ndescribe('Authentication Events', () => {\n  beforeEach(() => {\n    mockJwtDecode.mockClear()\n    mockStore.get.mockClear()\n    mockStore.set.mockClear()\n    mockStore.delete.mockClear()\n    mockGrpcClient.setAuthToken.mockClear()\n    mockSyncService.start.mockClear()\n    mockSyncService.stop.mockClear()\n    mockMainWindow.isDestroyed.mockClear()\n    mockMainWindow.webContents.send.mockClear()\n    mockFetch.mockClear()\n  })\n\n  describe('shouldRefreshToken', () => {\n    test('should return true when token expires within 5 minutes', () => {\n      const fourMinutesFromNow = Date.now() + 4 * 60 * 1000\n\n      expect(shouldRefreshToken(fourMinutesFromNow)).toBe(true)\n    })\n\n    test('should return false when token expires after 5 minutes', () => {\n      const sixMinutesFromNow = Date.now() + 6 * 60 * 1000\n\n      expect(shouldRefreshToken(sixMinutesFromNow)).toBe(false)\n    })\n\n    test('should return true for already expired tokens', () => {\n      const oneHourAgo = Date.now() - 60 * 60 * 1000\n\n      expect(shouldRefreshToken(oneHourAgo)).toBe(true)\n    })\n\n    test('should return true at exactly 5 minute boundary', () => {\n      const exactlyFiveMinutes = Date.now() + 5 * 60 * 1000\n\n      expect(shouldRefreshToken(exactlyFiveMinutes)).toBe(true)\n    })\n\n    test('should handle edge case at boundary', () => {\n      const almostFiveMinutes = Date.now() + 5 * 60 * 1000 - 1\n\n      expect(shouldRefreshToken(almostFiveMinutes)).toBe(true)\n    })\n  })\n\n  describe('isTokenExpired', () => {\n    beforeEach(() => {\n      mockJwtDecode.mockClear()\n    })\n\n    test('should return false for valid non-expired token', () => {\n      const futureExp = Math.floor(Date.now() / 1000) + 3600 // 1 hour from now\n      mockJwtDecode.mockReturnValue({ exp: futureExp })\n\n      expect(isTokenExpired('valid-token')).toBe(false)\n    })\n\n    test('should return true for expired token', () => {\n      const pastExp = Math.floor(Date.now() / 1000) - 3600 // 1 hour ago\n      mockJwtDecode.mockReturnValue({ exp: pastExp })\n\n      expect(isTokenExpired('expired-token')).toBe(true)\n    })\n\n    test('should return true for token without exp field', () => {\n      mockJwtDecode.mockReturnValue({ sub: 'user-123' }) // No exp field\n\n      expect(isTokenExpired('token-without-exp')).toBe(true)\n    })\n\n    test('should return true when JWT decode fails', () => {\n      mockJwtDecode.mockImplementation(() => {\n        throw new Error('Invalid JWT')\n      })\n\n      expect(isTokenExpired('malformed-token')).toBe(true)\n    })\n\n    test('should handle token at exact expiration boundary', () => {\n      const currentTime = Math.floor(Date.now() / 1000)\n      mockJwtDecode.mockReturnValue({ exp: currentTime })\n\n      expect(isTokenExpired('boundary-token')).toBe(false)\n    })\n  })\n\n  describe('exchangeAuthCode business logic', () => {\n    test('should reject mismatched state parameter (CSRF protection)', async () => {\n      // Setup: Store has one state, request has different state\n      mockStore.get.mockReturnValue({\n        state: {\n          codeVerifier: 'valid-verifier',\n          state: 'stored-state-123',\n        },\n      })\n\n      const result = await exchangeAuthCode(\n        {},\n        {\n          authCode: 'valid-code',\n          state: 'different-state-456', // Mismatch!\n          config: {\n            domain: 'test.auth0.com',\n            clientId: 'client123',\n            redirectUri: 'http://localhost/callback',\n          },\n        },\n      )\n\n      expect(result.success).toBe(false)\n      expect(result.error).toContain('State mismatch')\n      expect(mockFetch).not.toHaveBeenCalled() // Should not make network call\n    })\n\n    test('should reject missing code verifier (PKCE requirement)', async () => {\n      // Setup: Store missing code verifier\n      mockStore.get.mockReturnValue({\n        state: {\n          state: 'matching-state',\n          // codeVerifier is missing - PKCE violation\n        },\n      })\n\n      const result = await exchangeAuthCode(\n        {},\n        {\n          authCode: 'valid-code',\n          state: 'matching-state',\n          config: {\n            domain: 'test.auth0.com',\n            clientId: 'client123',\n            redirectUri: 'http://localhost/callback',\n          },\n        },\n      )\n\n      expect(result.success).toBe(false)\n      expect(result.error).toContain('Code verifier not found')\n      expect(mockFetch).not.toHaveBeenCalled() // Should not make network call\n    })\n\n    test('should handle network errors gracefully', async () => {\n      // Setup: Valid auth state\n      mockStore.get.mockReturnValue({\n        state: {\n          codeVerifier: 'valid-verifier',\n          state: 'matching-state',\n        },\n      })\n\n      // Mock network failure\n      mockFetch.mockRejectedValue(new Error('Network timeout'))\n\n      const result = await exchangeAuthCode(\n        {},\n        {\n          authCode: 'valid-code',\n          state: 'matching-state',\n          config: {\n            domain: 'test.auth0.com',\n            clientId: 'client123',\n            redirectUri: 'http://localhost/callback',\n          },\n        },\n      )\n\n      expect(result.success).toBe(false)\n      expect(result.error).toContain('Network timeout')\n    })\n\n    test('should handle Auth0 API errors gracefully', async () => {\n      // Setup: Valid auth state\n      mockStore.get.mockReturnValue({\n        state: {\n          codeVerifier: 'valid-verifier',\n          state: 'matching-state',\n        },\n      })\n\n      // Mock Auth0 error response\n      mockFetch.mockResolvedValue({\n        ok: false,\n        status: 400,\n        statusText: 'Bad Request',\n        text: mock().mockResolvedValue('invalid_grant: Code has expired'),\n      })\n\n      const result = await exchangeAuthCode(\n        {},\n        {\n          authCode: 'expired-code',\n          state: 'matching-state',\n          config: {\n            domain: 'test.auth0.com',\n            clientId: 'client123',\n            redirectUri: 'http://localhost/callback',\n          },\n        },\n      )\n\n      expect(result.success).toBe(false)\n      expect(result.error).toContain('Token exchange failed')\n    })\n  })\n\n  describe('handleLogin business logic', () => {\n    const testProfile = {\n      id: 'user-123',\n      name: 'Test User',\n      email: 'test@example.com',\n    }\n\n    test('should always store user profile regardless of token presence', () => {\n      handleLogin(testProfile, null, null)\n\n      expect(mockStore.set).toHaveBeenCalledWith('userProfile', testProfile)\n    })\n\n    test('should store both tokens when both are provided', () => {\n      const idToken = 'id-token-123'\n      const accessToken = 'access-token-123'\n\n      handleLogin(testProfile, idToken, accessToken)\n\n      expect(mockStore.set).toHaveBeenCalledWith('userProfile', testProfile)\n      expect(mockStore.set).toHaveBeenCalledWith('idToken', idToken)\n      expect(mockStore.set).toHaveBeenCalledWith('accessToken', accessToken)\n    })\n\n    test('should only store ID token when access token is null', () => {\n      const idToken = 'id-token-123'\n\n      handleLogin(testProfile, idToken, null)\n\n      expect(mockStore.set).toHaveBeenCalledWith('userProfile', testProfile)\n      expect(mockStore.set).toHaveBeenCalledWith('idToken', idToken)\n      expect(mockStore.set).not.toHaveBeenCalledWith(\n        'accessToken',\n        expect.anything(),\n      )\n\n      // Should not start services without access token\n      expect(mockGrpcClient.setAuthToken).not.toHaveBeenCalled()\n      expect(mockSyncService.start).not.toHaveBeenCalled()\n    })\n\n    test('should only store access token when ID token is null', () => {\n      const accessToken = 'access-token-123'\n\n      handleLogin(testProfile, null, accessToken)\n\n      expect(mockStore.set).toHaveBeenCalledWith('userProfile', testProfile)\n      expect(mockStore.set).not.toHaveBeenCalledWith(\n        'idToken',\n        expect.anything(),\n      )\n      expect(mockStore.set).toHaveBeenCalledWith('accessToken', accessToken)\n\n      // Should start services with access token\n      expect(mockGrpcClient.setAuthToken).toHaveBeenCalledWith(accessToken)\n      expect(mockSyncService.start).toHaveBeenCalled()\n    })\n\n    test('should setup services only when access token is present', () => {\n      const accessToken = 'access-token-123'\n\n      handleLogin(testProfile, 'id-token', accessToken)\n\n      // Services should be configured with access token\n      expect(mockGrpcClient.setAuthToken).toHaveBeenCalledWith(accessToken)\n      expect(mockSyncService.start).toHaveBeenCalled()\n    })\n\n    test('should not setup services when no access token provided', () => {\n      handleLogin(testProfile, 'id-token', null)\n\n      // Services should not be started\n      expect(mockGrpcClient.setAuthToken).not.toHaveBeenCalled()\n      expect(mockSyncService.start).not.toHaveBeenCalled()\n    })\n\n    test('should setup services in correct order when access token present', () => {\n      const accessToken = 'access-token-123'\n\n      handleLogin(testProfile, 'id-token', accessToken)\n\n      // Verify order: store profile, store tokens, then setup services\n      const calls = mockStore.set.mock.calls\n      expect(calls[0]).toEqual(['userProfile', testProfile])\n      expect(calls[1]).toEqual(['idToken', 'id-token'])\n      expect(calls[2]).toEqual(['accessToken', accessToken])\n\n      // Service setup should happen after token storage\n      expect(mockGrpcClient.setAuthToken).toHaveBeenCalledWith(accessToken)\n      expect(mockSyncService.start).toHaveBeenCalled()\n    })\n  })\n\n  describe('handleLogout business logic', () => {\n    test('should clear all auth data and stop services', () => {\n      handleLogout()\n\n      // Should delete all stored auth data\n      expect(mockStore.delete).toHaveBeenCalledWith('userProfile')\n      expect(mockStore.delete).toHaveBeenCalledWith('idToken')\n      expect(mockStore.delete).toHaveBeenCalledWith('accessToken')\n\n      // Should clear gRPC auth and stop sync service\n      expect(mockGrpcClient.setAuthToken).toHaveBeenCalledWith(null)\n      expect(mockSyncService.stop).toHaveBeenCalled()\n    })\n  })\n\n  describe('refreshTokens business logic', () => {\n    const testConfig = { domain: 'test.auth0.com', clientId: 'client123' }\n\n    test('should calculate expiration timestamp correctly', async () => {\n      const beforeCall = Date.now()\n      const expiresInSeconds = 3600 // 1 hour\n\n      mockFetch.mockResolvedValue({\n        ok: true,\n        json: mock().mockResolvedValue({\n          access_token: 'new-access-token',\n          expires_in: expiresInSeconds,\n        }),\n      })\n\n      const result = await refreshTokens('refresh-token-123', testConfig)\n      const afterCall = Date.now()\n\n      expect(result.success).toBe(true)\n\n      // Expiration should be approximately now + expires_in seconds\n      const expectedMin = beforeCall + expiresInSeconds * 1000\n      const expectedMax = afterCall + expiresInSeconds * 1000\n\n      expect(result.tokens.expires_at).toBeGreaterThanOrEqual(expectedMin)\n      expect(result.tokens.expires_at).toBeLessThanOrEqual(expectedMax)\n    })\n\n    test('should preserve original refresh token when new one not provided', async () => {\n      const originalRefreshToken = 'original-refresh-token'\n\n      mockFetch.mockResolvedValue({\n        ok: true,\n        json: mock().mockResolvedValue({\n          access_token: 'new-access-token',\n          expires_in: 3600,\n          // No refresh_token in response\n        }),\n      })\n\n      const result = await refreshTokens(originalRefreshToken, testConfig)\n\n      expect(result.success).toBe(true)\n      expect(result.tokens.refresh_token).toBe(originalRefreshToken)\n    })\n\n    test('should use new refresh token when provided in response', async () => {\n      const newRefreshToken = 'new-refresh-token'\n\n      mockFetch.mockResolvedValue({\n        ok: true,\n        json: mock().mockResolvedValue({\n          access_token: 'new-access-token',\n          expires_in: 3600,\n          refresh_token: newRefreshToken,\n        }),\n      })\n\n      const result = await refreshTokens('old-refresh-token', testConfig)\n\n      expect(result.success).toBe(true)\n      expect(result.tokens.refresh_token).toBe(newRefreshToken)\n    })\n\n    test('should handle network errors gracefully', async () => {\n      mockFetch.mockRejectedValue(new Error('Network connection failed'))\n\n      const result = await refreshTokens('refresh-token-123', testConfig)\n\n      expect(result.success).toBe(false)\n      expect(result.error).toContain('Network connection failed')\n    })\n\n    test('should handle Auth0 API errors gracefully', async () => {\n      mockFetch.mockResolvedValue({\n        ok: false,\n        status: 400,\n        statusText: 'Bad Request',\n        text: mock().mockResolvedValue('invalid_grant'),\n      })\n\n      const result = await refreshTokens('invalid-refresh-token', testConfig)\n\n      expect(result.success).toBe(false)\n      expect(result.error).toContain('Token refresh failed')\n    })\n\n    test('should handle unknown errors gracefully', async () => {\n      mockFetch.mockRejectedValue('Some weird error object')\n\n      const result = await refreshTokens('refresh-token-123', testConfig)\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBe('Unknown error')\n    })\n  })\n\n  describe('ensureValidTokens business logic', () => {\n    const testConfig = { domain: 'test.auth0.com', clientId: 'client123' }\n\n    test('should return error when no stored auth exists', async () => {\n      mockStore.get.mockReturnValue(null)\n\n      const result = await ensureValidTokens(testConfig)\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBe('No refresh token available')\n    })\n\n    test('should return error when no tokens exist', async () => {\n      mockStore.get.mockReturnValue({ someOtherData: 'value' })\n\n      const result = await ensureValidTokens(testConfig)\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBe('No refresh token available')\n    })\n\n    test('should return error when no refresh token exists', async () => {\n      mockStore.get.mockReturnValue({\n        tokens: {\n          access_token: 'access-token',\n          expires_at: Date.now() + 10 * 60 * 1000,\n          // No refresh_token\n        },\n      })\n\n      const result = await ensureValidTokens(testConfig)\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBe('No refresh token available')\n    })\n\n    test('should return existing tokens when no refresh needed', async () => {\n      const validTokens = {\n        access_token: 'valid-access-token',\n        refresh_token: 'refresh-token',\n        expires_at: Date.now() + 10 * 60 * 1000, // 10 minutes - no refresh needed\n      }\n\n      mockStore.get.mockReturnValue({\n        tokens: validTokens,\n        isAuthenticated: true,\n      })\n\n      const result: any = await ensureValidTokens(testConfig)\n\n      expect(result.success).toBe(true)\n      expect(result.tokens).toBe(validTokens)\n      expect(mockFetch).not.toHaveBeenCalled() // Should not attempt refresh\n    })\n\n    test('should orchestrate successful token refresh', async () => {\n      const expiringTokens = {\n        access_token: 'expiring-access-token',\n        refresh_token: 'refresh-token',\n        expires_at: Date.now() + 2 * 60 * 1000, // 2 minutes - needs refresh\n      }\n\n      const storedAuth = {\n        tokens: expiringTokens,\n        isAuthenticated: true,\n        userId: 'user-123',\n      }\n\n      mockStore.get.mockReturnValue(storedAuth)\n      mockFetch.mockResolvedValue({\n        ok: true,\n        json: mock().mockResolvedValue({\n          access_token: 'new-access-token',\n          refresh_token: 'new-refresh-token',\n          expires_in: 3600,\n        }),\n      })\n\n      const result = await ensureValidTokens(testConfig)\n\n      expect(result.success).toBe(true)\n\n      // Should update auth store with new tokens\n      expect(mockStore.set).toHaveBeenCalledWith('auth', {\n        ...storedAuth,\n        tokens: expect.objectContaining({\n          access_token: 'new-access-token',\n          refresh_token: 'new-refresh-token',\n        }),\n      })\n\n      // Should update main store with new access token\n      expect(mockStore.set).toHaveBeenCalledWith(\n        'accessToken',\n        'new-access-token',\n      )\n\n      // Should update gRPC client\n      expect(mockGrpcClient.setAuthToken).toHaveBeenCalledWith(\n        'new-access-token',\n      )\n\n      // Should notify renderer\n      expect(mockMainWindow.webContents.send).toHaveBeenCalledWith(\n        'tokens-refreshed',\n        expect.objectContaining({\n          access_token: 'new-access-token',\n        }),\n      )\n    })\n\n    test('should orchestrate cleanup when refresh fails', async () => {\n      const expiringTokens = {\n        access_token: 'expiring-access-token',\n        refresh_token: 'invalid-refresh-token',\n        expires_at: Date.now() + 2 * 60 * 1000,\n      }\n\n      const storedAuth = {\n        tokens: expiringTokens,\n        isAuthenticated: true,\n      }\n\n      mockStore.get.mockReturnValue(storedAuth)\n      mockFetch.mockResolvedValue({\n        ok: false,\n        status: 400,\n        statusText: 'Bad Request',\n        text: mock().mockResolvedValue('invalid_grant'),\n      })\n\n      const result = await ensureValidTokens(testConfig)\n\n      expect(result.success).toBe(false)\n\n      // Should clear auth data (logout orchestration)\n      expect(mockStore.delete).toHaveBeenCalledWith('userProfile')\n      expect(mockStore.delete).toHaveBeenCalledWith('idToken')\n      expect(mockStore.delete).toHaveBeenCalledWith('accessToken')\n      expect(mockGrpcClient.setAuthToken).toHaveBeenCalledWith(null)\n      expect(mockSyncService.stop).toHaveBeenCalled()\n\n      // Should clear auth store\n      expect(mockStore.set).toHaveBeenCalledWith('auth', {\n        ...storedAuth,\n        tokens: null,\n        isAuthenticated: false,\n      })\n\n      // Should notify renderer about expiration\n      expect(mockMainWindow.webContents.send).toHaveBeenCalledWith(\n        'auth-token-expired',\n      )\n    })\n\n    test('should handle missing expires_at gracefully', async () => {\n      const tokensWithoutExpiry = {\n        access_token: 'access-token',\n        refresh_token: 'refresh-token',\n        // No expires_at field\n      }\n\n      mockStore.get.mockReturnValue({\n        tokens: tokensWithoutExpiry,\n      })\n\n      const result: any = await ensureValidTokens(testConfig)\n\n      expect(result.success).toBe(true)\n      expect(result.tokens).toBe(tokensWithoutExpiry)\n      expect(mockFetch).not.toHaveBeenCalled() // Should not attempt refresh\n    })\n  })\n})\n"
  },
  {
    "path": "lib/auth/events.ts",
    "content": "import store, { AuthState, createNewAuthState } from '../main/store'\nimport { STORE_KEYS } from '../constants/store-keys'\nimport mainStore from '../main/store'\nimport { grpcClient } from '../clients/grpcClient'\nimport { syncService } from '../main/syncService'\nimport { mainWindow } from '../main/app'\nimport { jwtDecode } from 'jwt-decode'\n\n// Define TypeScript interfaces for JWT payloads\ninterface JwtPayload {\n  exp?: number\n  iat?: number\n  sub?: string\n  email?: string\n  name?: string\n  picture?: string\n  iss?: string\n  aud?: string | string[]\n  [key: string]: any\n}\n\n// Utility function to check if a JWT token is expired\nexport const isTokenExpired = (token: string): boolean => {\n  try {\n    const payload = jwtDecode<JwtPayload>(token)\n\n    // Check if token has expired\n    const currentTime = Math.floor(Date.now() / 1000)\n    return payload.exp ? payload.exp < currentTime : true\n  } catch (error) {\n    console.warn('Failed to decode token for expiration check:', error)\n    // If we can't decode the token, assume it's expired to be safe\n    return true\n  }\n}\n\n// Check and validate stored tokens on startup\nexport const validateStoredTokens = async (config?: any) => {\n  try {\n    const storedAuth = store.get(STORE_KEYS.AUTH)\n    const storedTokens = storedAuth?.tokens\n    const mainStoreAccessToken = mainStore.get(STORE_KEYS.ACCESS_TOKEN) as\n      | string\n      | undefined\n\n    // Check if we have tokens to validate\n    const hasTokens = storedTokens?.access_token || mainStoreAccessToken\n\n    if (hasTokens) {\n      console.log('Checking stored access tokens for expiration...')\n\n      // First, ensure tokens match current environment (issuer/audience)\n      const tokenToCheck = mainStoreAccessToken || storedTokens?.access_token\n      if (config && tokenToCheck) {\n        let envMismatch = false\n        try {\n          const payload = jwtDecode<JwtPayload>(tokenToCheck)\n          const issuer = payload.iss || ''\n          const audience = payload.aud\n\n          // Determine expected issuer host from config.domain\n          const expectedDomain = (config.domain || '').toString().trim()\n          let issuerMatches = false\n          try {\n            const issUrl = new URL(issuer)\n            issuerMatches =\n              !!expectedDomain &&\n              issUrl.host.toLowerCase() === expectedDomain.toLowerCase()\n          } catch {\n            // If issuer is not a URL, do a loose contains match\n            issuerMatches = !!expectedDomain && issuer.includes(expectedDomain)\n          }\n\n          // Audience can be string or array\n          let audienceMatches = true\n          if (config.audience) {\n            const expectedAudience = config.audience.toString().trim()\n            const audiences = Array.isArray(audience)\n              ? audience\n              : audience\n                ? [audience]\n                : []\n            audienceMatches = audiences.some(a => a === expectedAudience)\n          }\n\n          envMismatch = !issuerMatches || !audienceMatches\n        } catch {\n          // If we fail to decode for env check, treat as mismatch to be safe\n          envMismatch = true\n        }\n\n        if (envMismatch) {\n          console.log(\n            'Stored access token does not match current environment, clearing auth data',\n          )\n\n          // Clear auth store tokens\n          if (storedAuth) {\n            store.set(STORE_KEYS.AUTH, {\n              ...storedAuth,\n              tokens: null,\n            })\n          }\n\n          // Clear gRPC client token and stop sync\n          grpcClient.setAuthToken(null)\n          syncService.stop()\n\n          // Clear main process store\n          mainStore.delete(STORE_KEYS.USER_PROFILE)\n          mainStore.delete(STORE_KEYS.ID_TOKEN)\n          mainStore.delete(STORE_KEYS.ACCESS_TOKEN)\n\n          // Notify renderer process about token expiration/mismatch\n          if (mainWindow && !mainWindow.isDestroyed()) {\n            mainWindow.webContents.send('auth-token-expired')\n          }\n\n          return false\n        }\n      }\n\n      // Check both token sources\n      const authStoreTokenExpired = storedTokens?.access_token\n        ? isTokenExpired(storedTokens.access_token)\n        : false\n      const mainStoreTokenExpired = mainStoreAccessToken\n        ? isTokenExpired(mainStoreAccessToken)\n        : false\n\n      if (authStoreTokenExpired || mainStoreTokenExpired) {\n        console.log('Stored access tokens are expired')\n\n        // Try to refresh tokens if we have a refresh token and config\n        if (storedTokens?.refresh_token && config) {\n          console.log('Attempting to refresh expired tokens...')\n\n          const refreshResult = await ensureValidTokens(config)\n\n          if (refreshResult.success) {\n            console.log('Successfully refreshed expired tokens')\n            return true\n          } else {\n            console.log('Token refresh failed, clearing auth data')\n          }\n        } else {\n          console.log('No refresh token available, clearing auth data')\n        }\n\n        // Clear expired tokens from auth store\n        if (storedAuth) {\n          store.set(STORE_KEYS.AUTH, {\n            ...storedAuth,\n            tokens: null,\n          })\n        }\n\n        // Clear gRPC client token\n        grpcClient.setAuthToken(null)\n\n        // Stop sync service\n        syncService.stop()\n\n        // Clear main process store\n        mainStore.delete(STORE_KEYS.USER_PROFILE)\n        mainStore.delete(STORE_KEYS.ID_TOKEN)\n        mainStore.delete(STORE_KEYS.ACCESS_TOKEN)\n\n        // Notify renderer process about token expiration\n        if (mainWindow && !mainWindow.isDestroyed()) {\n          mainWindow.webContents.send('auth-token-expired')\n        } else {\n          // If main window isn't created yet, we'll need to notify it later\n          // The renderer will handle this when it initializes\n          console.log(\n            'Main window not available yet, token expiration will be handled on renderer init',\n          )\n        }\n\n        return false // Tokens were invalid\n      } else {\n        console.log('Stored access tokens are valid')\n\n        // Ensure both stores are in sync\n        if (storedTokens?.access_token && !mainStoreAccessToken) {\n          mainStore.set(STORE_KEYS.ACCESS_TOKEN, storedTokens.access_token)\n        } else if (mainStoreAccessToken && !storedTokens?.access_token) {\n          // Update auth store with main store token\n          if (storedAuth) {\n            store.set(STORE_KEYS.AUTH, {\n              ...storedAuth,\n              tokens: {\n                ...storedAuth.tokens,\n                access_token: mainStoreAccessToken,\n              },\n            })\n          }\n        }\n\n        return true // Tokens are valid\n      }\n    }\n\n    return true // No tokens to validate\n  } catch (error) {\n    console.error('Error validating stored tokens:', error)\n    return false // Assume invalid on error\n  }\n}\n\nexport const generateNewAuthState = (): AuthState => {\n  const newAuthState = createNewAuthState()\n\n  // Update the auth state in the store\n  const currentAuth = store.get(STORE_KEYS.AUTH)\n  store.set(STORE_KEYS.AUTH, {\n    ...currentAuth,\n    state: newAuthState,\n  })\n\n  return newAuthState\n}\n\n// Auth token exchange\nexport const exchangeAuthCode = async (_e, { authCode, state, config }) => {\n  try {\n    const authStore = store.get(STORE_KEYS.AUTH)\n    const codeVerifier = authStore.state?.codeVerifier\n    const storedState = authStore.state?.state\n\n    // Validate state parameter\n    if (storedState !== state) {\n      throw new Error(`State mismatch: expected ${storedState}, got ${state}`)\n    }\n\n    if (!codeVerifier) {\n      throw new Error('Code verifier not found in store')\n    }\n\n    const tokenParams = new URLSearchParams({\n      grant_type: 'authorization_code',\n      client_id: config.clientId,\n      code: authCode,\n      redirect_uri: config.redirectUri,\n      code_verifier: codeVerifier,\n    })\n\n    // Add audience if present in config\n    if (config.audience) {\n      tokenParams.append('audience', config.audience)\n    }\n\n    const response = await fetch(`https://${config.domain}/oauth/token`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/x-www-form-urlencoded',\n      },\n      body: tokenParams.toString(),\n    })\n\n    if (!response.ok) {\n      const errorText = await response.text()\n      console.error('Token exchange failed:')\n      console.error('Status:', response.status)\n      console.error('Status Text:', response.statusText)\n      console.error('Response:', errorText)\n      console.error('Request params:', tokenParams.toString())\n\n      throw new Error(\n        `Token exchange failed: ${response.status} ${response.statusText} - ${errorText}`,\n      )\n    }\n\n    const tokens = await response.json()\n\n    // Extract user info from ID token if available\n    let userInfo: any = null\n    if (tokens.id_token) {\n      try {\n        const payload = jwtDecode<JwtPayload>(tokens.id_token)\n        userInfo = {\n          id: payload.sub,\n          email: payload.email,\n          name: payload.name,\n          picture: payload.picture,\n        }\n      } catch (jwtError) {\n        console.warn('Failed to decode ID token:', jwtError)\n      }\n    }\n\n    return {\n      success: true,\n      tokens,\n      userInfo,\n    }\n  } catch (error) {\n    console.error('Token exchange error in main process:', error)\n    return {\n      success: false,\n      error: error instanceof Error ? error.message : 'Unknown error',\n    }\n  }\n}\n\nexport const handleLogin = (\n  profile: any,\n  idToken: string | null,\n  accessToken: string | null,\n) => {\n  mainStore.set(STORE_KEYS.USER_PROFILE, profile)\n\n  if (idToken) {\n    mainStore.set(STORE_KEYS.ID_TOKEN, idToken)\n  }\n\n  if (accessToken) {\n    mainStore.set(STORE_KEYS.ACCESS_TOKEN, accessToken)\n    grpcClient.setAuthToken(accessToken)\n    syncService.start()\n  }\n\n  // For self-hosted users, we don't start sync service since they don't have tokens\n}\n\nexport const handleLogout = () => {\n  mainStore.delete(STORE_KEYS.USER_PROFILE)\n  mainStore.delete(STORE_KEYS.ID_TOKEN)\n  mainStore.delete(STORE_KEYS.ACCESS_TOKEN)\n  grpcClient.setAuthToken(null)\n  syncService.stop()\n}\n\nexport const refreshTokens = async (refreshToken: string, config: any) => {\n  try {\n    const tokenParams = new URLSearchParams({\n      grant_type: 'refresh_token',\n      client_id: config.clientId,\n      refresh_token: refreshToken,\n    })\n\n    // Add audience if present in config\n    if (config.audience) {\n      tokenParams.append('audience', config.audience)\n    }\n\n    const response = await fetch(`https://${config.domain}/oauth/token`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/x-www-form-urlencoded',\n      },\n      body: tokenParams.toString(),\n    })\n\n    if (!response.ok) {\n      const errorText = await response.text()\n      console.error('Token refresh failed:', response.status, errorText)\n      throw new Error(\n        `Token refresh failed: ${response.status} ${response.statusText}`,\n      )\n    }\n\n    const tokens = await response.json()\n\n    // Calculate expiration time\n    const expiresAt = Date.now() + tokens.expires_in * 1000\n\n    return {\n      success: true,\n      tokens: {\n        ...tokens,\n        expires_at: expiresAt,\n        // Keep the original refresh token if not provided in response\n        refresh_token: tokens.refresh_token || refreshToken,\n      },\n    }\n  } catch (error) {\n    console.error('Token refresh error:', error)\n    return {\n      success: false,\n      error: error instanceof Error ? error.message : 'Unknown error',\n    }\n  }\n}\n\n// Check if token needs refresh (refresh 5 minutes before expiry)\nexport const shouldRefreshToken = (expiresAt: number): boolean => {\n  const fiveMinutes = 5 * 60 * 1000 // 5 minutes in milliseconds\n  return Date.now() >= expiresAt - fiveMinutes\n}\n\n// Automatically refresh tokens if needed\nexport const ensureValidTokens = async (config: any) => {\n  const storedAuth = store.get(STORE_KEYS.AUTH)\n  const tokens = storedAuth?.tokens\n\n  if (!tokens || !tokens.refresh_token) {\n    return { success: false, error: 'No refresh token available' }\n  }\n\n  // Check if token needs refresh\n  if (tokens.expires_at && shouldRefreshToken(tokens.expires_at)) {\n    console.log('Access token needs refresh, refreshing...')\n\n    const refreshResult = await refreshTokens(tokens.refresh_token, config)\n\n    if (refreshResult.success) {\n      // Update stored tokens\n      const updatedAuth = {\n        ...storedAuth,\n        tokens: refreshResult.tokens,\n      }\n\n      store.set(STORE_KEYS.AUTH, updatedAuth)\n\n      // Update main store\n      if (refreshResult.tokens.access_token) {\n        mainStore.set(\n          STORE_KEYS.ACCESS_TOKEN,\n          refreshResult.tokens.access_token,\n        )\n        grpcClient.setAuthToken(refreshResult.tokens.access_token)\n      }\n\n      // Notify renderer about token refresh\n      if (mainWindow && !mainWindow.isDestroyed()) {\n        mainWindow.webContents.send('tokens-refreshed', refreshResult.tokens)\n      }\n\n      return { success: true, tokens: refreshResult.tokens }\n    } else {\n      // Refresh failed, clear auth data\n      console.log('Token refresh failed, clearing auth data')\n      handleLogout()\n\n      // Clear auth store\n      store.set(STORE_KEYS.AUTH, {\n        ...storedAuth,\n        tokens: null,\n        isAuthenticated: false,\n      })\n\n      // Notify renderer about token expiration\n      if (mainWindow && !mainWindow.isDestroyed()) {\n        mainWindow.webContents.send('auth-token-expired')\n      }\n\n      return refreshResult\n    }\n  }\n\n  return { success: true, tokens }\n}\n"
  },
  {
    "path": "lib/clients/grpcClient.test.ts",
    "content": "import { describe, test, expect, beforeEach, mock, Mock } from 'bun:test'\nimport { ActiveWindow } from '../media/active-application'\nimport { ItoMode } from '@/app/generated/ito_pb'\n\n// Mock external dependencies to focus on core grpcClient logic\nconst mockElectronWindow = {\n  webContents: {\n    send: mock(),\n    isDestroyed: mock(() => false),\n  },\n  isDestroyed: mock(() => false),\n} as any\n\nconst mockSetFocusedText = mock()\nconst mockGetCurrentUserId = mock(() => 'test-user-123')\nconst mockDictionaryTable = {\n  findAll: mock(() =>\n    Promise.resolve([\n      { word: 'hello', deleted_at: null },\n      { word: 'world', deleted_at: null },\n    ]),\n  ),\n}\nconst mockEnsureValidTokens = mock(() =>\n  Promise.resolve({\n    success: true,\n    tokens: { access_token: 'new-token' },\n  }),\n)\n\nmock.module('../media/text-writer', () => ({\n  setFocusedText: mockSetFocusedText,\n}))\n\nconst mockStore = { get: mock(), set: mock(), delete: mock() }\n\nmock.module('../main/store', () => ({\n  default: mockStore,\n  store: mockStore,\n  getCurrentUserId: mockGetCurrentUserId,\n  createNewAuthState: mock(() => ({\n    state: 'test-state',\n    codeVerifier: 'test-verifier',\n  })),\n  getAdvancedSettings: mock(() => ({\n    llm: {\n      asrModel: 'whisper-large-v3',\n    },\n  })),\n}))\n\nmock.module('../main/sqlite/repo', () => ({\n  DictionaryTable: mockDictionaryTable,\n  NotesTable: {\n    insert: mock(() => Promise.resolve({ id: 'test-id' })),\n    findById: mock(() => Promise.resolve(undefined)),\n    findAll: mock(() => Promise.resolve([])),\n    findByInteractionId: mock(() => Promise.resolve([])),\n    updateContent: mock(() => Promise.resolve()),\n    softDelete: mock(() => Promise.resolve()),\n    deleteAllUserData: mock(() => Promise.resolve()),\n    upsert: mock(() => Promise.resolve()),\n    findModifiedSince: mock(() => Promise.resolve([])),\n  },\n  InteractionsTable: {\n    insert: mock(() => Promise.resolve({ id: 'test-id' })),\n    findById: mock(() => Promise.resolve(undefined)),\n    findAll: mock(() => Promise.resolve([])),\n    softDelete: mock(() => Promise.resolve()),\n    deleteAllUserData: mock(() => Promise.resolve()),\n    upsert: mock(() => Promise.resolve()),\n    findModifiedSince: mock(() => Promise.resolve([])),\n  },\n  KeyValueStore: {\n    get: mock(() => Promise.resolve(undefined)),\n    set: mock(() => Promise.resolve()),\n  },\n}))\n\nmock.module('../auth/events', () => ({\n  ensureValidTokens: mockEnsureValidTokens,\n  validateStoredTokens: mock(() => Promise.resolve(true)),\n  generateNewAuthState: mock(() => ({\n    state: 'test-state',\n    codeVerifier: 'test-verifier',\n  })),\n  exchangeAuthCode: mock(() => Promise.resolve({ success: true })),\n  handleLogin: mock(),\n  handleLogout: mock(),\n  refreshTokens: mock(() => Promise.resolve({ success: true })),\n  shouldRefreshToken: mock(() => false),\n}))\n\nmock.module('../auth/config', () => ({\n  Auth0Config: { domain: 'test.auth0.com' },\n}))\n\nmock.module('../media/selected-text-reader', () => ({\n  getSelectedTextString: mock(() => Promise.resolve('Selected text')),\n}))\n\nconst mockGetActiveWindow: Mock<() => Promise<ActiveWindow | null>> = mock(() =>\n  Promise.resolve({\n    title: 'Test App',\n    appName: 'TestApp',\n    windowId: 1,\n    processId: 1234,\n    positon: { x: 0, y: 0, width: 800, height: 600 },\n  }),\n)\nmock.module('../media/active-application', () => ({\n  getActiveWindow: mockGetActiveWindow,\n}))\n\n// Mock the entire gRPC stack to avoid network calls\nconst mockGrpcClientMethods = {\n  transcribeStream: mock(() =>\n    Promise.resolve({ transcript: 'test transcript' } as any),\n  ),\n  createNote: mock(() => Promise.resolve({ success: true } as any)),\n  updateNote: mock(() => Promise.resolve({ success: true } as any)),\n  deleteNote: mock(() => Promise.resolve({ success: true } as any)),\n  listNotes: mock(() => Promise.resolve({ notes: [] as any })),\n  createInteraction: mock(() => Promise.resolve({ success: true } as any)),\n  updateInteraction: mock(() => Promise.resolve({ success: true } as any)),\n  deleteInteraction: mock(() => Promise.resolve({ success: true } as any)),\n  listInteractions: mock(() => Promise.resolve({ interactions: [] as any })),\n  createDictionaryItem: mock(() => Promise.resolve({ success: true } as any)),\n  updateDictionaryItem: mock(() => Promise.resolve({ success: true } as any)),\n  deleteDictionaryItem: mock(() => Promise.resolve({ success: true } as any)),\n  listDictionaryItems: mock(() => Promise.resolve({ items: [] as any })),\n  deleteUserData: mock(() => Promise.resolve({ success: true } as any)),\n}\n\nmock.module('@connectrpc/connect', () => ({\n  createClient: mock(() => mockGrpcClientMethods),\n  ConnectError: class MockConnectError extends Error {\n    code: number\n    constructor(message: string, code: number) {\n      super(message)\n      this.code = code\n    }\n  },\n  Code: {\n    Unauthenticated: 16,\n    InvalidArgument: 3,\n    NotFound: 5,\n    Internal: 13,\n  },\n}))\n\nmock.module('@connectrpc/connect-node', () => ({\n  createConnectTransport: mock(() => ({})),\n}))\n\nmock.module('@bufbuild/protobuf', () => ({\n  create: mock((_schema: any, data: any) => data),\n}))\n\n// Mock protobuf schemas\nmock.module('@/app/generated/ito_pb', () => ({\n  ItoService: { typeName: 'ItoService' },\n  // Mock all the schema objects\n  CreateNoteRequestSchema: { typeName: 'CreateNoteRequest' },\n  UpdateNoteRequestSchema: { typeName: 'UpdateNoteRequest' },\n  DeleteNoteRequestSchema: { typeName: 'DeleteNoteRequest' },\n  ListNotesRequestSchema: { typeName: 'ListNotesRequest' },\n  CreateInteractionRequestSchema: { typeName: 'CreateInteractionRequest' },\n  UpdateInteractionRequestSchema: { typeName: 'UpdateInteractionRequest' },\n  DeleteInteractionRequestSchema: { typeName: 'DeleteInteractionRequest' },\n  ListInteractionsRequestSchema: { typeName: 'ListInteractionsRequest' },\n  CreateDictionaryItemRequestSchema: {\n    typeName: 'CreateDictionaryItemRequest',\n  },\n  UpdateDictionaryItemRequestSchema: {\n    typeName: 'UpdateDictionaryItemRequest',\n  },\n  DeleteDictionaryItemRequestSchema: {\n    typeName: 'DeleteDictionaryItemRequest',\n  },\n  ListDictionaryItemsRequestSchema: { typeName: 'ListDictionaryItemsRequest' },\n  DeleteUserDataRequestSchema: { typeName: 'DeleteUserDataRequest' },\n  UpdateAdvancedSettingsRequestSchema: {\n    typeName: 'UpdateAdvancedSettingsRequest',\n  },\n  GetAdvancedSettingsRequestSchema: { typeName: 'GetAdvancedSettingsRequest' },\n}))\n\n// Mock console to avoid noise\nbeforeEach(() => {\n  console.log = mock()\n  console.error = mock()\n  console.info = mock()\n})\n\ndescribe('GrpcClient Business Logic Tests', () => {\n  beforeEach(() => {\n    // Reset all mocks\n    Object.values(mockGrpcClientMethods).forEach(mock => mock.mockClear())\n    mockElectronWindow.webContents.send.mockClear()\n    mockElectronWindow.isDestroyed.mockClear()\n    mockSetFocusedText.mockClear()\n    mockGetCurrentUserId.mockClear()\n    mockDictionaryTable.findAll.mockClear()\n    mockEnsureValidTokens.mockClear()\n    mockGetActiveWindow.mockClear()\n\n    // Reset default behaviors\n    mockElectronWindow.isDestroyed.mockReturnValue(false)\n    mockGetCurrentUserId.mockReturnValue('test-user-123')\n    mockDictionaryTable.findAll.mockResolvedValue([\n      { word: 'hello', deleted_at: null },\n      { word: 'world', deleted_at: null },\n    ])\n    mockEnsureValidTokens.mockResolvedValue({\n      success: true,\n      tokens: { access_token: 'refreshed-token' },\n    })\n  })\n\n  describe('Transcription Stream Business Logic', () => {\n    test('should handle transcription stream with text output and side effects', async () => {\n      const { grpcClient } = await import('./grpcClient')\n      grpcClient.setAuthToken('test-token')\n      grpcClient.setMainWindow(mockElectronWindow)\n\n      const transcript = 'Hello world, this is a test'\n      mockGrpcClientMethods.transcribeStream.mockResolvedValueOnce({\n        transcript,\n      })\n\n      const audioStream = (async function* () {\n        yield { data: new Uint8Array([1, 2, 3]) } as any\n        yield { data: new Uint8Array([4, 5, 6]) } as any\n      })()\n\n      const result = await grpcClient.transcribeStream(\n        audioStream,\n        ItoMode.TRANSCRIBE,\n      )\n\n      expect(result.transcript).toBe(transcript)\n    })\n\n    test('should include metadata in transcription', async () => {\n      const { grpcClient } = await import('./grpcClient')\n      grpcClient.setAuthToken('test-token')\n\n      const audioStream = (async function* () {\n        yield { data: new Uint8Array([1, 2, 3]) } as any\n      })()\n\n      await grpcClient.transcribeStream(audioStream, ItoMode.TRANSCRIBE)\n\n      expect(mockDictionaryTable.findAll).toHaveBeenCalledWith('test-user-123')\n      expect(mockGrpcClientMethods.transcribeStream).toHaveBeenCalled()\n      expect(mockGetActiveWindow).toHaveBeenCalled()\n    })\n\n    test('if window context fails to be retrieved, it should handle gracefully', async () => {\n      const { grpcClient } = await import('./grpcClient')\n      grpcClient.setAuthToken('test-token')\n\n      const audioStream = (async function* () {\n        yield { data: new Uint8Array([1, 2, 3]) } as any\n      })()\n\n      mockGetActiveWindow.mockResolvedValueOnce(null)\n\n      await grpcClient.transcribeStream(audioStream, ItoMode.TRANSCRIBE)\n      expect(mockGetActiveWindow).toHaveBeenCalled()\n    })\n\n    test('should handle transcription errors gracefully', async () => {\n      const { grpcClient } = await import('./grpcClient')\n      grpcClient.setAuthToken('test-token')\n      grpcClient.setMainWindow(mockElectronWindow)\n\n      const error = new Error('Transcription failed')\n      mockGrpcClientMethods.transcribeStream.mockRejectedValueOnce(error)\n\n      const audioStream = (async function* () {\n        yield { data: new Uint8Array([1, 2, 3]) } as any\n      })()\n\n      expect(\n        grpcClient.transcribeStream(audioStream, ItoMode.TRANSCRIBE),\n      ).rejects.toThrow('Transcription failed')\n    })\n\n    test('should handle vocabulary fetch errors during transcription', async () => {\n      const { grpcClient } = await import('./grpcClient')\n      grpcClient.setAuthToken('test-token')\n\n      mockDictionaryTable.findAll.mockRejectedValueOnce(\n        new Error('Database error'),\n      )\n\n      const audioStream = (async function* () {\n        yield { data: new Uint8Array([1, 2, 3]) } as any\n      })()\n\n      await grpcClient.transcribeStream(audioStream, ItoMode.TRANSCRIBE)\n      expect(mockGrpcClientMethods.transcribeStream).toHaveBeenCalled()\n    })\n  })\n\n  describe('Authentication', () => {\n    test('should handle operations with no auth token gracefully', async () => {\n      const { grpcClient } = await import('./grpcClient')\n      grpcClient.setAuthToken(null)\n\n      const testNote = {\n        id: 'note-123',\n        content: 'Test note',\n        interaction_id: null,\n        user_id: 'test-user',\n        created_at: '2024-01-01T00:00:00.000Z',\n        updated_at: '2024-01-01T00:00:00.000Z',\n        deleted_at: null,\n      }\n\n      // Should proceed with operation (empty headers but no crash)\n      const result = await grpcClient.createNote(testNote)\n      expect(result).toBeDefined()\n    })\n\n    test('should handle auth errors gracefully when window is destroyed', async () => {\n      const { grpcClient } = await import('./grpcClient')\n      grpcClient.setAuthToken('test-token')\n      grpcClient.setMainWindow(mockElectronWindow)\n\n      // Mock window as destroyed\n      mockElectronWindow.isDestroyed.mockReturnValue(true)\n\n      // Mock authentication error\n      const authError = new Error('Unauthenticated')\n      mockGrpcClientMethods.transcribeStream.mockRejectedValueOnce(authError)\n\n      const audioStream = (async function* () {\n        yield { data: new Uint8Array([1, 2, 3]) } as any\n      })()\n\n      // Should not crash when trying to send auth error to destroyed window\n      await expect(\n        grpcClient.transcribeStream(audioStream, ItoMode.TRANSCRIBE),\n      ).rejects.toThrow('Unauthenticated')\n      expect(mockElectronWindow.webContents.send).not.toHaveBeenCalled()\n    })\n  })\n})\n"
  },
  {
    "path": "lib/clients/grpcClient.ts",
    "content": "import {\n  ItoService,\n  TimingService,\n  AudioChunk,\n  Note as NotePb,\n  Interaction as InteractionPb,\n  DictionaryItem as DictionaryItemPb,\n  AdvancedSettings as AdvancedSettingsPb,\n  TimingReport,\n  TimingEvent,\n  CreateNoteRequestSchema,\n  UpdateNoteRequestSchema,\n  DeleteNoteRequestSchema,\n  ListNotesRequestSchema,\n  CreateInteractionRequestSchema,\n  UpdateInteractionRequestSchema,\n  DeleteInteractionRequestSchema,\n  ListInteractionsRequestSchema,\n  CreateDictionaryItemRequestSchema,\n  DeleteDictionaryItemRequestSchema,\n  UpdateDictionaryItemRequestSchema,\n  ListDictionaryItemsRequestSchema,\n  DeleteUserDataRequestSchema,\n  GetAdvancedSettingsRequestSchema,\n  UpdateAdvancedSettingsRequestSchema,\n  SubmitTimingReportsRequestSchema,\n  TimingReportSchema,\n  TimingEventSchema,\n  ItoMode,\n  TranscribeStreamRequest,\n} from '@/app/generated/ito_pb'\nimport { createClient } from '@connectrpc/connect'\nimport { createConnectTransport } from '@connectrpc/connect-node'\nimport { ConnectError, Code } from '@connectrpc/connect'\nimport { BrowserWindow } from 'electron'\nimport { create } from '@bufbuild/protobuf'\nimport { Note, Interaction, DictionaryItem } from '../main/sqlite/models'\nimport { DictionaryTable } from '../main/sqlite/repo'\nimport {\n  AdvancedSettings,\n  getAdvancedSettings,\n  getCurrentUserId,\n  store,\n} from '../main/store'\nimport { getSelectedTextString } from '../media/selected-text-reader'\nimport { ensureValidTokens } from '../auth/events'\nimport { Auth0Config } from '../auth/config'\nimport { getActiveWindow } from '../media/active-application'\nimport { STORE_KEYS } from '../constants/store-keys.js'\n\nclass GrpcClient {\n  private client: ReturnType<typeof createClient<typeof ItoService>>\n  private timingClient: ReturnType<typeof createClient<typeof TimingService>>\n  private authToken: string | null = null\n  private mainWindow: BrowserWindow | null = null\n  private isRefreshingTokens: boolean = false\n\n  constructor() {\n    const transport = createConnectTransport({\n      baseUrl: import.meta.env.VITE_GRPC_BASE_URL,\n      httpVersion: '1.1',\n    })\n    console.log(\n      'Creating gRPC client with base URL:',\n      import.meta.env.VITE_GRPC_BASE_URL,\n    )\n    this.client = createClient(ItoService, transport)\n    this.timingClient = createClient(TimingService, transport)\n  }\n\n  setMainWindow(window: BrowserWindow) {\n    this.mainWindow = window\n  }\n\n  // Helper method to safely send messages to the main window\n  private safeSendToMainWindow(channel: string, ...args: any[]) {\n    if (\n      this.mainWindow &&\n      !this.mainWindow.isDestroyed() &&\n      !this.mainWindow.webContents.isDestroyed()\n    ) {\n      try {\n        this.mainWindow.webContents.send(channel, ...args)\n      } catch (error) {\n        console.warn(\n          `Failed to send message to main window on channel ${channel}:`,\n          error,\n        )\n        // Clear the reference to the destroyed window\n        this.mainWindow = null\n      }\n    }\n  }\n\n  setAuthToken(token: string | null) {\n    this.authToken = token\n  }\n\n  private getHeaders() {\n    if (!this.authToken) {\n      // Though we have guards elsewhere, this is a final check.\n      // Throwing here helps us pinpoint auth issues during development.\n      return new Headers()\n    }\n    return new Headers({ Authorization: `Bearer ${this.authToken}` })\n  }\n\n  private async getHeadersWithMetadata(mode: ItoMode) {\n    const headers = this.getHeaders()\n\n    try {\n      // Fetch vocabulary from local database\n      const user_id = getCurrentUserId()\n      const dictionaryItems = await DictionaryTable.findAll(user_id)\n\n      // Convert to vocabulary format for transcription\n      const vocabularyWords = dictionaryItems\n        .filter(item => item.deleted_at === null)\n        .map(item => item.word)\n\n      // Add vocabulary to headers if available\n      if (vocabularyWords.length > 0) {\n        headers.set('vocabulary', vocabularyWords.join(','))\n      }\n\n      // Fetch window context\n      const windowContext = await getActiveWindow()\n      if (windowContext) {\n        headers.set('window-title', windowContext.title)\n        headers.set('app-name', windowContext.appName)\n      }\n\n      function flattenHeaderValue(value: string) {\n        const flattened = value\n          .replace(/[\\r\\n]+/g, ' ')\n          .replace(/\\s{2,}/g, ' ')\n          .trim()\n\n        // Check if the string contains non-ASCII characters\n        // eslint-disable-next-line no-control-regex\n        const hasUnicode = /[^\\x00-\\x7F]/.test(flattened)\n\n        if (hasUnicode) {\n          // Base64 encode to safely transmit Unicode characters via gRPC headers\n          return `base64:${Buffer.from(flattened, 'utf8').toString('base64')}`\n        }\n\n        return flattened\n      }\n\n      // Add ASR model from advanced settings\n      const advancedSettings = getAdvancedSettings()\n      headers.set('asr-model', advancedSettings.llm.asrModel ?? '')\n      headers.set('asr-provider', advancedSettings.llm.asrProvider ?? '')\n      headers.set(\n        'asr-prompt',\n        flattenHeaderValue(advancedSettings.llm.asrPrompt ?? ''),\n      )\n      headers.set('llm-provider', advancedSettings.llm.llmProvider ?? '')\n      headers.set('llm-model', advancedSettings.llm.llmModel ?? '')\n      headers.set(\n        'llm-temperature',\n        advancedSettings.llm.llmTemperature?.toString() ?? '',\n      )\n      headers.set(\n        'transcription-prompt',\n        flattenHeaderValue(advancedSettings.llm.transcriptionPrompt ?? ''),\n      )\n      // Note: Editing prompt is currently disabled until a better versioning solution is implemented\n      // https://github.com/heyito/ito/issues/174\n      // headers.set(\n      //   'editing-prompt',\n      //   flattenHeaderValue(advancedSettings.llm.editingPrompt),\n      // )\n      headers.set(\n        'no-speech-threshold',\n        advancedSettings.llm.noSpeechThreshold?.toString() ?? '',\n      )\n\n      headers.set('mode', mode.toString())\n\n      try {\n        // We currently only support context gathering on mac\n        if (mode === ItoMode.EDIT) {\n          const contextText = await getSelectedTextString(10000)\n          if (contextText && contextText.trim().length > 0) {\n            headers.set('context-text', flattenHeaderValue(contextText))\n            console.log(\n              '[gRPC Client] Adding context text to headers:',\n              contextText.length,\n              'characters',\n              contextText,\n            )\n          }\n        }\n      } catch (error) {\n        console.error('[gRPC Client] Error getting context text:', error)\n      }\n    } catch (error) {\n      console.error(\n        'Failed to fetch vocabulary/settings for transcription:',\n        error,\n      )\n    }\n\n    return headers\n  }\n\n  private async withRetry<T>(operation: () => Promise<T>): Promise<T> {\n    try {\n      return await operation()\n    } catch (error) {\n      const shouldRetry = await this.handleAuthError(error)\n\n      if (shouldRetry) {\n        console.log('Retrying operation after token refresh')\n        return await operation()\n      }\n\n      throw error\n    }\n  }\n\n  private async handleAuthError(error: any): Promise<boolean> {\n    // Check if this is an authentication error\n    if (error instanceof ConnectError && error.code === Code.Unauthenticated) {\n      console.log(\n        'Authentication error detected, attempting token refresh before logout',\n      )\n\n      // Prevent multiple simultaneous refresh attempts\n      if (this.isRefreshingTokens) {\n        console.log('Token refresh already in progress, skipping')\n        return false\n      }\n\n      try {\n        this.isRefreshingTokens = true\n\n        // Attempt to refresh tokens\n        const refreshResult = await ensureValidTokens(Auth0Config)\n\n        if (\n          refreshResult.success &&\n          'tokens' in refreshResult &&\n          refreshResult.tokens?.access_token\n        ) {\n          console.log('Token refresh successful, updating auth token')\n          this.authToken = refreshResult.tokens.access_token\n\n          // Return true to indicate the caller should retry\n          return true\n        } else {\n          console.log('Token refresh failed, proceeding with logout')\n        }\n      } catch (refreshError) {\n        console.error('Error during token refresh:', refreshError)\n      } finally {\n        this.isRefreshingTokens = false\n      }\n\n      // If we get here, token refresh failed - proceed with logout\n      console.log('Signing out user due to authentication failure')\n\n      // Notify the main window to sign out the user\n      this.safeSendToMainWindow('auth-token-expired')\n\n      // Clear the auth token\n      this.authToken = null\n    }\n\n    // Return false to indicate no retry should be attempted\n    return false\n  }\n\n  async transcribeStream(stream: AsyncIterable<AudioChunk>, mode: ItoMode) {\n    return this.withRetry(async () => {\n      const response = await this.client.transcribeStream(stream, {\n        headers: await this.getHeadersWithMetadata(mode),\n      })\n      return response\n    })\n  }\n\n  async transcribeStreamV2(\n    stream: AsyncIterable<TranscribeStreamRequest>,\n    signal?: AbortSignal,\n  ) {\n    return this.withRetry(async () => {\n      const response = await this.client.transcribeStreamV2(stream, {\n        headers: this.getHeaders(),\n        signal,\n      })\n      return response\n    })\n  }\n\n  // =================================================================\n  // Notes, Interactions, Dictionary (Unary Calls)\n  // =================================================================\n\n  async createNote(note: Note) {\n    return this.withRetry(async () => {\n      const request = create(CreateNoteRequestSchema, {\n        id: note.id,\n        interactionId: note.interaction_id ?? '',\n        content: note.content,\n      })\n      return await this.client.createNote(request, {\n        headers: this.getHeaders(),\n      })\n    })\n  }\n\n  async updateNote(note: Note) {\n    return this.withRetry(async () => {\n      const request = create(UpdateNoteRequestSchema, {\n        id: note.id,\n        content: note.content,\n      })\n      return await this.client.updateNote(request, {\n        headers: this.getHeaders(),\n      })\n    })\n  }\n\n  async deleteNote(note: Note) {\n    return this.withRetry(async () => {\n      const request = create(DeleteNoteRequestSchema, {\n        id: note.id,\n      })\n      return await this.client.deleteNote(request, {\n        headers: this.getHeaders(),\n      })\n    })\n  }\n\n  async listNotesSince(since?: string): Promise<NotePb[]> {\n    return this.withRetry(async () => {\n      const request = create(ListNotesRequestSchema, {\n        sinceTimestamp: since ?? '',\n      })\n      const response = await this.client.listNotes(request, {\n        headers: this.getHeaders(),\n      })\n      return response.notes\n    })\n  }\n\n  async createInteraction(interaction: Interaction) {\n    return this.withRetry(async () => {\n      // Convert Buffer to Uint8Array for protobuf\n      let uint8AudioData: Uint8Array\n      if (interaction.raw_audio) {\n        uint8AudioData = new Uint8Array(interaction.raw_audio)\n      } else {\n        uint8AudioData = new Uint8Array()\n      }\n\n      const request = create(CreateInteractionRequestSchema, {\n        id: interaction.id,\n        title: interaction.title ?? '',\n        asrOutput: JSON.stringify(interaction.asr_output),\n        llmOutput: JSON.stringify(interaction.llm_output),\n        rawAudio: uint8AudioData,\n        durationMs: interaction.duration_ms ?? 0,\n      })\n\n      console.log(\n        '[gRPC Client] Sending request with audio size:',\n        request.rawAudio.length,\n        'duration:',\n        request.durationMs,\n        'ms',\n      )\n\n      return await this.client.createInteraction(request, {\n        headers: this.getHeaders(),\n      })\n    })\n  }\n\n  async updateInteraction(interaction: Interaction) {\n    return this.withRetry(async () => {\n      const request = create(UpdateInteractionRequestSchema, {\n        id: interaction.id,\n        title: interaction.title ?? '',\n      })\n      return await this.client.updateInteraction(request, {\n        headers: this.getHeaders(),\n      })\n    })\n  }\n\n  async deleteInteraction(interaction: Interaction) {\n    return this.withRetry(async () => {\n      const request = create(DeleteInteractionRequestSchema, {\n        id: interaction.id,\n      })\n      return await this.client.deleteInteraction(request, {\n        headers: this.getHeaders(),\n      })\n    })\n  }\n\n  async listInteractionsSince(since?: string): Promise<InteractionPb[]> {\n    return this.withRetry(async () => {\n      const request = create(ListInteractionsRequestSchema, {\n        sinceTimestamp: since ?? '',\n      })\n      const response = await this.client.listInteractions(request, {\n        headers: this.getHeaders(),\n      })\n      return response.interactions\n    })\n  }\n\n  async createDictionaryItem(item: DictionaryItem) {\n    return this.withRetry(async () => {\n      const request = create(CreateDictionaryItemRequestSchema, {\n        id: item.id,\n        word: item.word,\n        pronunciation: item.pronunciation ?? '',\n      })\n      return await this.client.createDictionaryItem(request, {\n        headers: this.getHeaders(),\n      })\n    })\n  }\n\n  async updateDictionaryItem(item: DictionaryItem) {\n    return this.withRetry(async () => {\n      const request = create(UpdateDictionaryItemRequestSchema, {\n        id: item.id,\n        word: item.word,\n        pronunciation: item.pronunciation ?? '',\n      })\n      return await this.client.updateDictionaryItem(request, {\n        headers: this.getHeaders(),\n      })\n    })\n  }\n\n  async deleteDictionaryItem(item: DictionaryItem) {\n    return this.withRetry(async () => {\n      const request = create(DeleteDictionaryItemRequestSchema, {\n        id: item.id,\n      })\n      return await this.client.deleteDictionaryItem(request, {\n        headers: this.getHeaders(),\n      })\n    })\n  }\n\n  async listDictionaryItemsSince(since?: string): Promise<DictionaryItemPb[]> {\n    return this.withRetry(async () => {\n      const request = create(ListDictionaryItemsRequestSchema, {\n        sinceTimestamp: since ?? '',\n      })\n      const response = await this.client.listDictionaryItems(request, {\n        headers: this.getHeaders(),\n      })\n      return response.items\n    })\n  }\n\n  async deleteUserData() {\n    return this.withRetry(async () => {\n      const request = create(DeleteUserDataRequestSchema, {})\n      return await this.client.deleteUserData(request, {\n        headers: this.getHeaders(),\n      })\n    })\n  }\n\n  async getAdvancedSettings(): Promise<AdvancedSettingsPb | null> {\n    // Check if user is self-hosted and skip server sync\n    const userId = getCurrentUserId()\n    const isSelfHosted = userId === 'self-hosted'\n\n    if (isSelfHosted) {\n      console.log('Self-hosted user detected, using local advanced settings')\n      // Return null for self-hosted users since they don't sync with server\n      return null\n    }\n\n    return this.withRetry(async () => {\n      const request = create(GetAdvancedSettingsRequestSchema, {})\n      return await this.client.getAdvancedSettings(request, {\n        headers: this.getHeaders(),\n      })\n    })\n  }\n\n  async updateAdvancedSettings(\n    settings: AdvancedSettings,\n  ): Promise<AdvancedSettingsPb | null> {\n    // Check if user is self-hosted and skip server sync\n    const userId = getCurrentUserId()\n    const isSelfHosted = userId === 'self-hosted'\n\n    if (isSelfHosted) {\n      console.log(\n        'Self-hosted user detected, skipping server sync for advanced settings',\n      )\n      // Return null for self-hosted users since settings are stored locally\n      return null\n    }\n\n    console.log('Updating advanced settings:', settings.llm)\n\n    return this.withRetry(async () => {\n      const request = create(UpdateAdvancedSettingsRequestSchema, {\n        llm: {\n          asrModel: settings.llm.asrModel ?? undefined,\n          asrProvider: settings.llm.asrProvider ?? undefined,\n          asrPrompt: settings.llm.asrPrompt ?? undefined,\n          llmProvider: settings.llm.llmProvider ?? undefined,\n          llmModel: settings.llm.llmModel ?? undefined,\n          transcriptionPrompt: settings.llm.transcriptionPrompt ?? undefined,\n          editingPrompt: settings.llm.editingPrompt ?? undefined,\n          llmTemperature: settings.llm.llmTemperature ?? undefined,\n          noSpeechThreshold: settings.llm.noSpeechThreshold ?? undefined,\n        },\n      })\n      return await this.client.updateAdvancedSettings(request, {\n        headers: this.getHeaders(),\n      })\n    })\n  }\n\n  async submitTimingReports(reports: TimingReport[]) {\n    return this.withRetry(async () => {\n      const request = create(SubmitTimingReportsRequestSchema, {\n        reports,\n      })\n      return await this.timingClient.submitTimingReports(request, {\n        headers: this.getHeaders(),\n      })\n    })\n  }\n}\n\nexport const grpcClient = new GrpcClient()\n"
  },
  {
    "path": "lib/clients/itoHttpClient.ts",
    "content": "import store from '../main/store'\nimport { STORE_KEYS } from '../constants/store-keys'\n\ninterface RequestOptions {\n  requireAuth?: boolean\n  headers?: Record<string, string>\n}\n\n/**\n * HTTP client for Ito backend API calls\n */\nclass ItoHttpClient {\n  private getBaseUrl(): string {\n    return import.meta.env.VITE_GRPC_BASE_URL\n  }\n\n  private getAccessToken(): string {\n    return (store.get(STORE_KEYS.ACCESS_TOKEN) as string | null) || ''\n  }\n\n  async get(path: string, options: RequestOptions = {}) {\n    try {\n      const { requireAuth = false, headers = {} } = options\n      const token = this.getAccessToken()\n\n      if (requireAuth && !token) {\n        return { success: false, error: 'Access token not available' }\n      }\n\n      const url = new URL(path, this.getBaseUrl())\n      const response = await fetch(url.toString(), {\n        headers: {\n          ...headers,\n          ...(token ? { Authorization: `Bearer ${token}` } : {}),\n        },\n      })\n\n      const data: any = await response.json().catch(() => undefined)\n\n      if (!response.ok) {\n        return {\n          success: false,\n          error: data?.error || `Request failed (${response.status})`,\n          status: response.status,\n        }\n      }\n\n      return data\n    } catch (error: any) {\n      return { success: false, error: error?.message || 'Network error' }\n    }\n  }\n\n  async post(path: string, body?: any, options: RequestOptions = {}) {\n    try {\n      const { requireAuth = false, headers = {} } = options\n      const token = this.getAccessToken()\n\n      if (requireAuth && !token) {\n        return { success: false, error: 'Access token not available' }\n      }\n\n      const url = new URL(path, this.getBaseUrl())\n      const response = await fetch(url.toString(), {\n        method: 'POST',\n        headers: {\n          ...(body && { 'content-type': 'application/json' }),\n          ...headers,\n          ...(token ? { Authorization: `Bearer ${token}` } : {}),\n        },\n        ...(body && { body: JSON.stringify(body) }),\n      })\n\n      const data: any = await response.json().catch(() => undefined)\n\n      if (!response.ok) {\n        return {\n          success: false,\n          error: data?.error || `Request failed (${response.status})`,\n          status: response.status,\n        }\n      }\n\n      return data\n    } catch (error: any) {\n      return { success: false, error: error?.message || 'Network error' }\n    }\n  }\n}\n\nexport const itoHttpClient = new ItoHttpClient()\n"
  },
  {
    "path": "lib/constants/external-links.ts",
    "content": "export const EXTERNAL_LINKS = {\n  DISCORD: 'https://discord.gg/PME7NH38sn',\n  TEAM_CALL: 'https://link.heyito.ai/calendly',\n  X_TWITTER: 'https://x.com/ItoAssists',\n  GITHUB: 'https://github.com/heyito/ito',\n  WEBSITE: 'https://www.heyito.ai/',\n  PRIVACY_POLICY: 'https://www.heyito.ai/privacy',\n} as const\n"
  },
  {
    "path": "lib/constants/generated-defaults.ts",
    "content": "/*\n * AUTO-GENERATED FILE - DO NOT EDIT\n * Generated from /shared-constants.js\n * Run 'bun generate:constants' to regenerate\n */\n\nexport const DEFAULT_ADVANCED_SETTINGS = {\n  // ASR (Automatic Speech Recognition) settings\n  asrProvider: 'groq',\n  asrModel: 'whisper-large-v3',\n  asrPrompt: ``,\n\n  // LLM (Large Language Model) settings\n  llmProvider: 'groq',\n  llmModel: 'openai/gpt-oss-120b',\n  llmTemperature: 0.1,\n\n  // Prompt settings\n  transcriptionPrompt: `You are a real-time Transcript Polisher assistant. Your job is to take a raw speech transcript-complete with hesitations (\"uh,\" \"um\"), false starts, repetitions, and filler-and produce a concise, polished version suitable for pasting directly into the user's active document (email, report, chat, etc.).\n\n- Keep the user's meaning and tone intact: don't introduce ideas or change intent.\n- Remove disfluencies: delete \"uh,\" \"um,\" \"you know,\" repeated words, and false starts.\n- Resolve corrections smoothly: when the speaker self-corrects (\"let's do next week... no, next month\"), choose the final phrasing.\n- Preserve natural phrasing: maintain contractions and informal tone if present, unless clarity demands adjustment.\n- Maintain accuracy: do not invent or omit key details like dates, names, or numbers.\n- Produce clean prose: use complete sentences, correct punctuation, and paragraph breaks only where needed for readability.\n- Operate within a single reply: output only the cleaned text-no commentary, meta-notes, or apologies.\n\nExample\nRaw transcript:\n\"Uhhh, so, I was thinking... maybe we could-uh-shoot for Thursday morning? No, actually, let's aim for the first week of May.\"\n\nCleaned output:\n\"Let's schedule the meeting for the first week of May.\"\n\nWhen you receive a transcript, immediately return the polished version following these rules.\n`,\n  editingPrompt: ` You are a Command-Interpreter assistant. Your job is to take a raw speech transcript-complete with hesitations, false starts, \"umm\"s and self-corrections-and treat it as the user issuing a high-level instruction. Instead of merely polishing their words, you must:\n    1.\tExtract the intent: identify the action the user is asking for (e.g. \"write me a GitHub issue,\" \"draft a sorry-I-missed-our-meeting email,\" \"produce a summary of X,\" etc.).\n    2.\tIgnore disfluencies: strip out \"uh,\" \"um,\" false starts and filler so you see only the core command.\n    3.\tMap to a template: choose an appropriate standard format (GitHub issue markdown template, professional email, bullet-point agenda, etc.) that matches the intent.\n    4.\tGenerate the deliverable: produce a fully-formed document in that format, filling in placeholders sensibly from any details in the transcript.\n    5.\tDo not add new intent: if the transcript doesn't specify something (e.g. title, recipients, date), use reasonable defaults (e.g. \"Untitled Issue,\" \"To: [Recipient]\") or prompt the user for the missing piece.\n    6.\tProduce only the final document: no commentary, apologies, or side-notes-just the completed issue/email/summary/etc.\n    7. Your response MUST contain ONLY the resultant text. DO NOT include:\n      - Any markers like [START/END CURRENT NOTES CONTENT]\n      - Any explanations, apologies, or additional text\n      - Any formatting markers like --- or \\`\\`\\`\n  `,\n\n  // Audio quality thresholds\n  noSpeechThreshold: 0.6,\n} as const\n"
  },
  {
    "path": "lib/constants/keyboard-defaults.ts",
    "content": "import { ItoMode } from '@/app/generated/ito_pb'\n\n// Platform-specific keyboard shortcut defaults\nexport const ITO_MODE_SHORTCUT_DEFAULTS_MAC = {\n  [ItoMode.TRANSCRIBE]: ['fn'],\n  [ItoMode.EDIT]: ['control-left', 'fn'],\n}\n\nexport const ITO_MODE_SHORTCUT_DEFAULTS_WIN = {\n  [ItoMode.TRANSCRIBE]: ['control-left', 'command-left'],\n  [ItoMode.EDIT]: ['option-left', 'control-left'],\n}\n\n// Helper to detect platform - works in both main and renderer process\nexport function getPlatform(): 'darwin' | 'win32' {\n  if (typeof process !== 'undefined' && process.platform) {\n    return process.platform as 'darwin' | 'win32'\n  }\n  // Fallback if process is not available\n  return 'darwin'\n}\n\n// Get platform-specific defaults\nexport function getItoModeShortcutDefaults(\n  platform?: 'darwin' | 'win32',\n): Record<ItoMode, string[]> {\n  const currentPlatform = platform || getPlatform()\n\n  if (currentPlatform === 'darwin') {\n    return ITO_MODE_SHORTCUT_DEFAULTS_MAC\n  } else {\n    return ITO_MODE_SHORTCUT_DEFAULTS_WIN\n  }\n}\n\n// For backward compatibility, export the defaults for the current platform\nexport const ITO_MODE_SHORTCUT_DEFAULTS = getItoModeShortcutDefaults()\n"
  },
  {
    "path": "lib/constants/store-keys.test.ts",
    "content": "import { describe, test, expect } from 'bun:test'\nimport { STORE_KEYS } from './store-keys'\n\ndescribe('STORE_KEYS', () => {\n  test('should contain all expected keys', () => {\n    expect(STORE_KEYS.AUTH).toBe('auth')\n    expect(STORE_KEYS.USER_PROFILE).toBe('userProfile')\n    expect(STORE_KEYS.ID_TOKEN).toBe('idToken')\n    expect(STORE_KEYS.ACCESS_TOKEN).toBe('accessToken')\n    expect(STORE_KEYS.MAIN).toBe('main')\n    expect(STORE_KEYS.ONBOARDING).toBe('onboarding')\n    expect(STORE_KEYS.SETTINGS).toBe('settings')\n    expect(STORE_KEYS.OPEN_MIC).toBe('openMic')\n    expect(STORE_KEYS.SELECTED_AUDIO_INPUT).toBe('selectedAudioInput')\n    expect(STORE_KEYS.INTERACTION_SOUNDS).toBe('interactionSounds')\n  })\n})\n"
  },
  {
    "path": "lib/constants/store-keys.ts",
    "content": "// Store key constants to avoid magic strings\n// This file can be imported by both main and renderer processes\nexport const STORE_KEYS = {\n  AUTH: 'auth',\n  USER_PROFILE: 'userProfile',\n  ID_TOKEN: 'idToken',\n  ACCESS_TOKEN: 'accessToken',\n  MAIN: 'main',\n  ONBOARDING: 'onboarding',\n  SETTINGS: 'settings',\n  ADVANCED_SETTINGS: 'advancedSettings',\n  OPEN_MIC: 'openMic',\n  SELECTED_AUDIO_INPUT: 'selectedAudioInput',\n  INTERACTION_SOUNDS: 'interactionSounds',\n} as const\n\nexport type StoreKey = (typeof STORE_KEYS)[keyof typeof STORE_KEYS]\n"
  },
  {
    "path": "lib/main/app.ts",
    "content": "import { BrowserWindow, shell, screen, app, protocol, net } from 'electron'\nimport { join } from 'path'\nimport appIcon from '@/resources/build/icon.png?asset'\nimport { pathToFileURL } from 'url'\n\n// Keep a reference to the pill window to prevent it from being garbage collected.\nlet pillWindow: BrowserWindow | null = null\n// Keep a reference to the main window\nexport let mainWindow: BrowserWindow | null = null\n\nexport function getPillWindow(): BrowserWindow | null {\n  return pillWindow\n}\n\n// --- No changes to createAppWindow ---\nexport function createAppWindow(): BrowserWindow {\n  // Create the main window.\n  mainWindow = new BrowserWindow({\n    width: 1270,\n    height: 800,\n    show: false,\n    backgroundColor: '#ffffff',\n    icon: appIcon,\n    frame: false,\n    titleBarStyle: 'hiddenInset',\n    trafficLightPosition: { x: 20, y: 17 },\n    title: 'Ito',\n    maximizable: false,\n    resizable: false,\n    webPreferences: {\n      preload: join(__dirname, '../preload/preload.js'),\n      sandbox: false,\n      webSecurity: true,\n    },\n  })\n\n  mainWindow.on('ready-to-show', () => {\n    mainWindow!.show()\n  })\n\n  mainWindow.webContents.setWindowOpenHandler(details => {\n    shell.openExternal(details.url)\n    return { action: 'deny' }\n  })\n\n  mainWindow.webContents.session.webRequest.onHeadersReceived(\n    (details, callback) => {\n      callback({\n        responseHeaders: {\n          ...details.responseHeaders,\n          'Content-Security-Policy': [\n            \"default-src 'self' 'unsafe-inline' 'unsafe-eval'; \" +\n              \"connect-src 'self' https://*.posthog.com https://app.posthog.com https://eu.posthog.com https://us.i.posthog.com; \" +\n              \"script-src 'self' 'unsafe-inline' 'unsafe-eval'; \" +\n              \"style-src 'self' 'unsafe-inline'; \" +\n              \"img-src 'self' data: res:; \" +\n              \"media-src 'self' blob:;\",\n          ],\n        },\n      })\n    },\n  )\n\n  // Clean up the reference when the window is closed.\n  mainWindow.on('closed', () => {\n    mainWindow = null\n    // On Windows, closing the main window should quit the entire app\n    if (process.platform === 'win32') {\n      app.quit()\n    }\n  })\n\n  // HMR for renderer base on electron-vite cli.\n  // Load the remote URL for development or the local html file for production.\n  if (!app.isPackaged && process.env['ELECTRON_RENDERER_URL']) {\n    mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])\n  } else {\n    mainWindow.loadFile(join(__dirname, '../renderer/index.html'))\n  }\n\n  return mainWindow\n}\n\nconst PILL_MAX_WIDTH = 172\nconst PILL_MAX_HEIGHT = 84\nexport function createPillWindow(): void {\n  pillWindow = new BrowserWindow({\n    width: PILL_MAX_WIDTH,\n    height: PILL_MAX_HEIGHT,\n    show: true,\n    frame: false,\n    transparent: true,\n    alwaysOnTop: true, // Keep on top\n    skipTaskbar: true,\n    resizable: false,\n    maximizable: false,\n    minimizable: false,\n    focusable: false, // Prevents window from stealing focus\n    hasShadow: false,\n    type: 'panel',\n    acceptFirstMouse: true,\n    webPreferences: {\n      preload: join(__dirname, '../preload/preload.js'),\n      sandbox: false,\n    },\n    hiddenInMissionControl: true,\n  })\n\n  pillWindow.setIgnoreMouseEvents(true, { forward: true })\n\n  // Set properties for macOS to ensure it stays on top of full-screen apps\n  if (process.platform === 'darwin') {\n    pillWindow.setAlwaysOnTop(true, 'screen-saver', 1)\n    pillWindow.setFullScreenable(false)\n\n    pillWindow.setVisibleOnAllWorkspaces(true, {\n      visibleOnFullScreen: true,\n      skipTransformProcessType: true,\n    })\n  }\n\n  // Use a URL hash to tell our React app to load the pill component.\n  const pillUrl =\n    !app.isPackaged && process.env['ELECTRON_RENDERER_URL']\n      ? `${process.env['ELECTRON_RENDERER_URL']}/#/pill`\n      : `${pathToFileURL(join(__dirname, '../renderer/index.html'))}#/pill`\n\n  pillWindow.loadURL(pillUrl)\n\n  // Uncomment the next line to open the DevTools for debugging the pill window.\n  // This is useful during development to inspect the pill's UI and behavior.\n  // pillWindow.webContents.openDevTools({ mode: 'detach' })\n\n  // Clean up the reference when the window is closed.\n  pillWindow.on('closed', () => {\n    pillWindow = null\n  })\n}\n\nexport function startPillPositioner() {\n  // Listen for display changes to handle dock visibility changes\n  screen.on('display-metrics-changed', () => {\n    updatePillPosition()\n  })\n\n  // Initial position on start\n  updatePillPosition()\n\n  // Throttle updates (Windows needs less frequent updates to avoid jitter)\n  const intervalMs = process.platform === 'win32' ? 750 : 250\n  setInterval(updatePillPosition, intervalMs)\n}\n\nfunction updatePillPosition() {\n  if (!pillWindow) return\n\n  try {\n    // Get the display that the mouse cursor is currently on.\n    const point = screen.getCursorScreenPoint()\n    const display = screen.getDisplayNearestPoint(point)\n    const { width: pillWidth, height: pillHeight } = pillWindow.getBounds()\n\n    // Use workArea instead of bounds to account for dock/menu bar\n    const { x, y, width, height } = display.workArea\n    const screenBounds = display.bounds\n\n    const scale = display.scaleFactor || 1\n    const roundToDeviceDip = (v: number) =>\n      Math.round(Math.round(v * scale) / scale)\n\n    // Calculate position: Horizontally centered, positioned above taskbar\n    const newX = roundToDeviceDip(x + width / 2 - pillWidth / 2)\n\n    // Position just above the work area bottom\n    const newY = roundToDeviceDip(y + height - pillHeight - 10)\n\n    // Ensure we don't go below the screen bounds\n    const maxY = screenBounds.y + screenBounds.height - pillHeight - 10\n    const finalY = Math.min(newY, maxY)\n\n    // Only move if position actually changed (>= 1 DIP)\n    const [currX, currY] = pillWindow.getPosition()\n    if (Math.abs(currX - newX) < 1 && Math.abs(currY - finalY) < 1) {\n      return\n    }\n\n    // Set the position of the pill window.\n    pillWindow.setPosition(newX, finalY, false) // `false` = not animated\n  } catch (error) {\n    // This can fail if the app is starting up or shutting down.\n    console.warn('Could not update pill position:', error)\n  }\n}\n\n// --- No changes to other functions ---\nexport function registerResourcesProtocol() {\n  protocol.handle('res', async request => {\n    try {\n      const url = new URL(request.url)\n      // Combine hostname and pathname to get the full path\n      const fullPath = join(url.hostname, url.pathname.slice(1))\n      const filePath = join(__dirname, '../../resources', fullPath)\n      return net.fetch(pathToFileURL(filePath).toString())\n    } catch (error) {\n      console.error('Protocol error:', error)\n      return new Response('Resource not found', { status: 404 })\n    }\n  })\n}\n"
  },
  {
    "path": "lib/main/appNap.ts",
    "content": "import { powerSaveBlocker } from 'electron'\n\nlet powerSaveBlockerId: number | null = null\n\n// Prevent the system from going to sleep\nexport function preventAppNap() {\n  if (powerSaveBlockerId === null) {\n    powerSaveBlockerId = powerSaveBlocker.start('prevent-app-suspension')\n  }\n}\n\n// Allow the system to sleep again\nexport function allowAppNap() {\n  if (powerSaveBlockerId !== null) {\n    powerSaveBlocker.stop(powerSaveBlockerId)\n    powerSaveBlockerId = null\n  }\n}\n"
  },
  {
    "path": "lib/main/audio/AudioStreamManager.test.ts",
    "content": "import { describe, test, expect, beforeEach } from 'bun:test'\nimport { AudioStreamManager } from './AudioStreamManager'\nimport { audioRecorderService } from '../../media/audio'\n\ndescribe('AudioStreamManager', () => {\n  let audioManager: AudioStreamManager\n\n  beforeEach(() => {\n    audioRecorderService.removeAllListeners('audio-chunk')\n    audioRecorderService.removeAllListeners('audio-config')\n    audioManager = new AudioStreamManager()\n  })\n\n  describe('Basic Streaming Control', () => {\n    test('should start and stop streaming correctly', () => {\n      expect(audioManager.isCurrentlyStreaming()).toBe(false)\n\n      audioManager.initialize()\n      expect(audioManager.isCurrentlyStreaming()).toBe(true)\n\n      audioManager.stopStreaming()\n      expect(audioManager.isCurrentlyStreaming()).toBe(false)\n    })\n\n    test('should clear audio on start', () => {\n      // Add some chunks\n      audioManager.initialize()\n      audioManager.addAudioChunk(Buffer.from('test'))\n      audioManager.stopStreaming()\n\n      // Start again - should be clean\n      audioManager.initialize()\n      const buffer = audioManager.getInteractionAudioBuffer()\n      expect(buffer.length).toBe(0)\n    })\n  })\n\n  describe('Audio Chunk Management', () => {\n    test('should accumulate audio chunks', () => {\n      audioManager.initialize()\n\n      const chunk1 = Buffer.from('chunk1')\n      const chunk2 = Buffer.from('chunk2')\n\n      audioManager.addAudioChunk(chunk1)\n      audioManager.addAudioChunk(chunk2)\n\n      const buffer = audioManager.getInteractionAudioBuffer()\n      expect(buffer).toEqual(Buffer.concat([chunk1, chunk2]))\n    })\n\n    test('should ignore chunks when not streaming', () => {\n      const chunk = Buffer.from('test')\n      audioManager.addAudioChunk(chunk)\n\n      const buffer = audioManager.getInteractionAudioBuffer()\n      expect(buffer.length).toBe(0)\n    })\n\n    test('should clear interaction audio', () => {\n      audioManager.initialize()\n      audioManager.addAudioChunk(Buffer.from('test'))\n\n      audioManager.clearInteractionAudio()\n      const buffer = audioManager.getInteractionAudioBuffer()\n      expect(buffer.length).toBe(0)\n    })\n  })\n\n  describe('Audio Configuration', () => {\n    test('should set sample rate', () => {\n      expect(audioManager.getCurrentSampleRate()).toBe(16000) // default\n\n      audioManager.setAudioConfig({ sampleRate: 44100 })\n      expect(audioManager.getCurrentSampleRate()).toBe(44100)\n    })\n\n    test('should ignore invalid sample rates', () => {\n      audioManager.setAudioConfig({ sampleRate: 0 })\n      expect(audioManager.getCurrentSampleRate()).toBe(16000) // unchanged\n\n      audioManager.setAudioConfig({ sampleRate: -1 })\n      expect(audioManager.getCurrentSampleRate()).toBe(16000) // unchanged\n    })\n  })\n\n  describe('Audio Duration Calculation', () => {\n    test('should calculate duration correctly for 16kHz audio', () => {\n      audioManager.initialize()\n\n      // 16kHz, 16-bit mono = 2 bytes per sample\n      // 1600 samples = 0.1 seconds = 100ms\n      const bytes = 1600 * 2 // 3200 bytes\n      const chunk = Buffer.alloc(bytes)\n\n      audioManager.addAudioChunk(chunk)\n      expect(audioManager.getAudioDurationMs()).toBe(100)\n    })\n\n    test('should calculate duration correctly for different sample rates', () => {\n      audioManager.setAudioConfig({ sampleRate: 8000 })\n      audioManager.initialize()\n\n      // 8kHz, 16-bit mono = 2 bytes per sample\n      // 800 samples = 0.1 seconds = 100ms\n      const bytes = 800 * 2 // 1600 bytes\n      const chunk = Buffer.alloc(bytes)\n\n      audioManager.addAudioChunk(chunk)\n      expect(audioManager.getAudioDurationMs()).toBe(100)\n    })\n\n    test('should return zero duration for no audio', () => {\n      audioManager.initialize()\n      expect(audioManager.getAudioDurationMs()).toBe(0)\n    })\n  })\n\n  describe('Audio Streaming', () => {\n    test('should stream chunks immediately as they arrive', async () => {\n      audioManager.initialize()\n\n      const streamPromise = audioManager.streamAudioChunks()\n      const iterator = streamPromise[Symbol.asyncIterator]()\n\n      // Add a chunk\n      const chunk = Buffer.alloc(100)\n      audioManager.addAudioChunk(chunk)\n\n      // Should yield immediately (no minimum duration wait)\n      const result = await iterator.next()\n      expect(result.done).toBe(false)\n      expect(result.value).toHaveProperty('audioData')\n      expect(result.value?.audioData).toEqual(chunk)\n    })\n\n    test('should continue streaming additional chunks', async () => {\n      audioManager.initialize()\n\n      const streamPromise = audioManager.streamAudioChunks()\n      const iterator = streamPromise[Symbol.asyncIterator]()\n\n      // Add first chunk\n      const chunk1 = Buffer.from('chunk1')\n      audioManager.addAudioChunk(chunk1)\n\n      // Get first chunk\n      const result1 = await iterator.next()\n      expect(result1.done).toBe(false)\n      expect(result1.value?.audioData).toEqual(chunk1)\n\n      // Add second chunk\n      const chunk2 = Buffer.from('chunk2')\n      audioManager.addAudioChunk(chunk2)\n\n      // Should yield the second chunk\n      const result2 = await iterator.next()\n      expect(result2.done).toBe(false)\n      expect(result2.value?.audioData).toEqual(chunk2)\n    })\n\n    test('should finish streaming when stopped', async () => {\n      audioManager.initialize()\n\n      const chunk = Buffer.alloc(100)\n      audioManager.addAudioChunk(chunk)\n\n      const streamPromise = audioManager.streamAudioChunks()\n      const iterator = streamPromise[Symbol.asyncIterator]()\n\n      // Get first chunk\n      await iterator.next()\n\n      // Stop streaming\n      audioManager.stopStreaming()\n\n      // Should finish\n      const result = await iterator.next()\n      expect(result.done).toBe(true)\n    })\n\n    test('should wait for chunks when queue is empty', async () => {\n      audioManager.initialize()\n\n      const streamPromise = audioManager.streamAudioChunks()\n      const iterator = streamPromise[Symbol.asyncIterator]()\n\n      // Create a promise that races between next() and a timeout\n      const timeoutPromise = new Promise(resolve =>\n        setTimeout(() => resolve('timeout'), 50),\n      )\n\n      // Should wait (timeout) because no chunks have been added\n      const result = await Promise.race([iterator.next(), timeoutPromise])\n      expect(result).toBe('timeout')\n    })\n\n    test('should resume streaming after waiting for chunks', async () => {\n      audioManager.initialize()\n\n      const streamPromise = audioManager.streamAudioChunks()\n      const iterator = streamPromise[Symbol.asyncIterator]()\n\n      // Start the iteration (will wait for chunks)\n      const nextPromise = iterator.next()\n\n      // Add a chunk after a short delay\n      setTimeout(() => {\n        const chunk = Buffer.from('delayed-chunk')\n        audioManager.addAudioChunk(chunk)\n      }, 10)\n\n      // Should eventually receive the chunk\n      const result = await nextPromise\n      expect(result.done).toBe(false)\n      expect(result.value?.audioData).toEqual(Buffer.from('delayed-chunk'))\n    })\n  })\n\n  describe('Edge Cases', () => {\n    test('should handle empty chunks', () => {\n      audioManager.initialize()\n\n      const emptyChunk = Buffer.alloc(0)\n      audioManager.addAudioChunk(emptyChunk)\n\n      expect(audioManager.getAudioDurationMs()).toBe(0)\n    })\n\n    test('should handle very small chunks', () => {\n      audioManager.initialize()\n\n      const tinyChunk = Buffer.alloc(1) // 1 byte\n      audioManager.addAudioChunk(tinyChunk)\n\n      // Should be < 1ms duration\n      expect(audioManager.getAudioDurationMs()).toBe(0) // Floors to 0\n    })\n\n    test('should reset audio duration on restart', () => {\n      audioManager.initialize()\n      audioManager.addAudioChunk(Buffer.alloc(3200)) // 100ms\n      expect(audioManager.getAudioDurationMs()).toBe(100)\n\n      audioManager.stopStreaming()\n      audioManager.initialize()\n\n      expect(audioManager.getAudioDurationMs()).toBe(0)\n    })\n\n    test('should accumulate duration across multiple chunks', () => {\n      audioManager.initialize()\n\n      // Add 50ms worth (800 samples * 2 bytes)\n      audioManager.addAudioChunk(Buffer.alloc(1600))\n      expect(audioManager.getAudioDurationMs()).toBe(50)\n\n      // Add another 50ms\n      audioManager.addAudioChunk(Buffer.alloc(1600))\n      expect(audioManager.getAudioDurationMs()).toBe(100)\n    })\n  })\n\n  describe('Interaction Audio Buffer', () => {\n    test('should maintain complete audio buffer for interaction', () => {\n      audioManager.initialize()\n\n      const chunk1 = Buffer.from('audio1')\n      const chunk2 = Buffer.from('audio2')\n      const chunk3 = Buffer.from('audio3')\n\n      audioManager.addAudioChunk(chunk1)\n      audioManager.addAudioChunk(chunk2)\n      audioManager.addAudioChunk(chunk3)\n\n      const buffer = audioManager.getInteractionAudioBuffer()\n      expect(buffer).toEqual(Buffer.concat([chunk1, chunk2, chunk3]))\n    })\n\n    test('should preserve interaction buffer even after streaming stops', () => {\n      audioManager.initialize()\n\n      const chunk = Buffer.from('preserved')\n      audioManager.addAudioChunk(chunk)\n\n      audioManager.stopStreaming()\n\n      const buffer = audioManager.getInteractionAudioBuffer()\n      expect(buffer).toEqual(chunk)\n    })\n\n    test('should clear buffer only on explicit clear or restart', () => {\n      audioManager.initialize()\n      audioManager.addAudioChunk(Buffer.from('test'))\n\n      audioManager.stopStreaming()\n\n      // Buffer should still exist\n      expect(audioManager.getInteractionAudioBuffer().length).toBeGreaterThan(0)\n\n      // Clear explicitly\n      audioManager.clearInteractionAudio()\n      expect(audioManager.getInteractionAudioBuffer().length).toBe(0)\n    })\n  })\n})\n"
  },
  {
    "path": "lib/main/audio/AudioStreamManager.ts",
    "content": "import { AudioChunkSchema } from '@/app/generated/ito_pb'\nimport { create } from '@bufbuild/protobuf'\nimport { audioRecorderService } from '../../media/audio'\n\nexport class AudioStreamManager {\n  private isStreaming = false\n  private audioChunkQueue: Buffer[] = []\n  private resolveNewChunk: ((value: void | PromiseLike<void>) => void) | null =\n    null\n  private audioChunksForInteraction: Buffer[] = []\n  private currentSampleRate: number = 16000\n\n  async *streamAudioChunks() {\n    // Stream audio chunks as they arrive\n    while (this.isStreaming || this.audioChunkQueue.length > 0) {\n      if (this.audioChunkQueue.length === 0) {\n        if (this.isStreaming) {\n          await new Promise<void>(resolve => {\n            this.resolveNewChunk = resolve\n          })\n        } else {\n          break\n        }\n      }\n\n      while (this.audioChunkQueue.length > 0) {\n        const chunk = this.audioChunkQueue.shift()\n        if (chunk) {\n          yield create(AudioChunkSchema, { audioData: chunk })\n        }\n      }\n    }\n  }\n\n  initialize() {\n    this.isStreaming = true\n    this.audioChunkQueue = []\n    this.audioChunksForInteraction = []\n    this.setupListeners()\n  }\n\n  stopStreaming() {\n    this.isStreaming = false\n    if (this.resolveNewChunk) {\n      this.resolveNewChunk()\n      this.resolveNewChunk = null\n    }\n    this.removeListeners()\n  }\n\n  private setupListeners() {\n    console.log('[AudioStreamManager] Setting up audio listeners')\n    audioRecorderService.on('audio-chunk', this.handleAudioChunk)\n    audioRecorderService.on('audio-config', this.handleAudioConfig)\n  }\n\n  private removeListeners() {\n    console.log('[AudioStreamManager] Removing audio listeners')\n    audioRecorderService.off('audio-chunk', this.handleAudioChunk)\n    audioRecorderService.off('audio-config', this.handleAudioConfig)\n  }\n\n  private handleAudioChunk = (chunk: Buffer) => {\n    this.addAudioChunk(chunk)\n  }\n\n  private handleAudioConfig = ({ outputSampleRate, sampleRate }: any) => {\n    const effectiveRate = outputSampleRate || sampleRate || 16000\n    console.log('[AudioStreamManager] Received audio config:', {\n      outputSampleRate,\n      sampleRate,\n      effectiveRate,\n    })\n    this.setAudioConfig({ sampleRate: effectiveRate })\n  }\n\n  addAudioChunk(chunk: Buffer) {\n    if (!this.isStreaming) {\n      return\n    }\n\n    this.audioChunkQueue.push(chunk)\n    this.audioChunksForInteraction.push(chunk)\n\n    if (this.resolveNewChunk) {\n      this.resolveNewChunk()\n      this.resolveNewChunk = null\n    }\n  }\n\n  getInteractionAudioBuffer(): Buffer {\n    return Buffer.concat(this.audioChunksForInteraction)\n  }\n\n  setAudioConfig(config: { sampleRate?: number; channels?: number }) {\n    if (typeof config.sampleRate === 'number' && config.sampleRate > 0) {\n      this.currentSampleRate = config.sampleRate\n    }\n  }\n\n  getCurrentSampleRate(): number {\n    return this.currentSampleRate\n  }\n\n  isCurrentlyStreaming(): boolean {\n    return this.isStreaming\n  }\n\n  clearInteractionAudio() {\n    this.audioChunksForInteraction = []\n  }\n\n  getAudioDurationMs(): number {\n    const totalBytes = this.audioChunksForInteraction.reduce(\n      (sum, chunk) => sum + chunk.length,\n      0,\n    )\n    // 16-bit PCM mono -> 2 bytes per sample\n    const totalSamples = totalBytes / 2\n    const durationSeconds = totalSamples / this.currentSampleRate\n    return Math.floor(durationSeconds * 1000)\n  }\n}\n"
  },
  {
    "path": "lib/main/autoUpdaterWrapper.ts",
    "content": "import { app } from 'electron'\nimport log from 'electron-log'\nimport { autoUpdater } from 'electron-updater'\nimport { mainWindow } from './app'\nimport { hardKillAll, teardown } from './teardown'\nimport { ITO_ENV } from './env'\n\nexport interface UpdateStatus {\n  updateAvailable: boolean\n  updateDownloaded: boolean\n}\n\nlet updateStatus: UpdateStatus = {\n  updateAvailable: false,\n  updateDownloaded: false,\n}\n\nexport function getUpdateStatus(): UpdateStatus {\n  return { ...updateStatus }\n}\n\nexport function initializeAutoUpdater() {\n  // Initialize update status tracking\n  updateStatus = {\n    updateAvailable: false,\n    updateDownloaded: false,\n  }\n\n  // Allow auto-updater in development mode if VITE_DEV_AUTO_UPDATE is set\n  const enableDevUpdater = import.meta.env.VITE_DEV_AUTO_UPDATE === 'true'\n\n  if (app.isPackaged || enableDevUpdater) {\n    try {\n      console.log(\n        app.isPackaged\n          ? 'App is packaged, initializing auto updater...'\n          : 'Development auto-updater enabled, initializing...',\n      )\n\n      const bucket = import.meta.env.VITE_UPDATER_BUCKET\n      if (!bucket) {\n        throw new Error('VITE_UPDATER_BUCKET environment variable is not set')\n      }\n\n      // Force dev updates if in development mode\n      if (!app.isPackaged) {\n        autoUpdater.forceDevUpdateConfig = true\n      }\n\n      autoUpdater.setFeedURL({\n        provider: 's3',\n        bucket,\n        path: 'releases/',\n        region: 'us-west-2',\n      })\n\n      log.transports.file.level = 'debug'\n      autoUpdater.logger = log\n\n      autoUpdater.autoRunAppAfterInstall = true\n      autoUpdater.autoDownload = true\n      autoUpdater.autoInstallOnAppQuit = false\n\n      setupAutoUpdaterEvents()\n      autoUpdater.checkForUpdates()\n\n      // Poll for updates every 10 minutes\n      setInterval(\n        () => {\n          autoUpdater.checkForUpdates()\n        },\n        10 * 60 * 1000,\n      )\n    } catch (e) {\n      console.error('Failed to check for auto updates:', e)\n    }\n  }\n}\n\nfunction setupAutoUpdaterEvents() {\n  autoUpdater.on('update-available', () => {\n    updateStatus.updateAvailable = true\n    if (\n      mainWindow &&\n      !mainWindow.isDestroyed() &&\n      !mainWindow.webContents.isDestroyed()\n    ) {\n      mainWindow.webContents.send('update-available')\n    }\n  })\n\n  autoUpdater.on('update-downloaded', () => {\n    console.log('update downloaded successfully')\n    updateStatus.updateDownloaded = true\n    if (\n      mainWindow &&\n      !mainWindow.isDestroyed() &&\n      !mainWindow.webContents.isDestroyed()\n    ) {\n      mainWindow.webContents.send('update-downloaded')\n    }\n  })\n\n  autoUpdater.on('error', error => {\n    console.error('Auto updater error:', error)\n  })\n\n  autoUpdater.on('download-progress', progressObj => {\n    const log_message = `Download speed: ${progressObj.bytesPerSecond} - Downloaded ${progressObj.percent.toFixed(2)}% (${progressObj.transferred}/${progressObj.total})`\n    console.log(log_message)\n  })\n}\n\nlet installing = false\n\nexport async function installUpdateNow() {\n  if (installing) return\n  installing = true\n  console.log('[Updater] Preparing to install…')\n\n  try {\n    // Try to gracefully shut down processes\n    teardown()\n    await new Promise(resolve => setTimeout(resolve, 1_500))\n\n    console.log('[Updater] Forcibly kill all straggler processes')\n    // Force-kill stragglers + crashpad/helpers\n    await hardKillAll()\n\n    console.log('[Updater] calling autoUpdater quit and install')\n    // Fire the installer (UI visible for debugging recommended)\n    autoUpdater.quitAndInstall(false /* isSilent */, true /* forceRunAfter */)\n  } catch (e) {\n    log.error('[Updater] installUpdateNow error', e)\n    // Try again, but don’t loop forever\n    try {\n      await hardKillAll()\n      autoUpdater.quitAndInstall(false, true)\n    } catch {\n      /* empty */\n    }\n  }\n}\n"
  },
  {
    "path": "lib/main/context/ContextGrabber.ts",
    "content": "import { ItoMode } from '@/app/generated/ito_pb'\nimport { DictionaryTable } from '../sqlite/repo'\nimport { getCurrentUserId, getAdvancedSettings } from '../store'\nimport { getActiveWindow } from '../../media/active-application'\nimport {\n  getSelectedTextString,\n  getCursorContext,\n} from '../../media/selected-text-reader'\nimport { canGetContextFromCurrentApp } from '../../utils/applicationDetection'\nimport log from 'electron-log'\nimport { timingCollector, TimingEventName } from '../timing/TimingCollector'\nimport { macOSAccessibilityContextProvider } from '../../media/macOSAccessibilityContextProvider'\n\nexport interface ContextData {\n  vocabularyWords: string[]\n  windowTitle: string\n  appName: string\n  contextText: string\n  advancedSettings: ReturnType<typeof getAdvancedSettings>\n}\n\n/**\n * ContextGrabber centralizes all context gathering logic for transcription streams.\n * It collects vocabulary, window info, selected text, and settings.\n */\nexport class ContextGrabber {\n  /**\n   * Gather all context data needed for a transcription stream\n   */\n  public async gatherContext(mode: ItoMode): Promise<ContextData> {\n    console.log('[ContextGrabber] Gathering context for mode:', mode)\n\n    // Get vocabulary words from dictionary\n    const vocabularyWords = await this.getVocabulary()\n\n    // Get active window context\n    const { windowTitle, appName } = await timingCollector.timeAsync(\n      TimingEventName.WINDOW_CONTEXT_GATHER,\n      async () => await this.getWindowContext(),\n    )\n\n    // Get selected text if in EDIT mode\n    const contextText = await this.getContextText(mode)\n\n    // Get advanced settings\n    const advancedSettings = getAdvancedSettings()\n\n    console.log('[ContextGrabber] Context gathered successfully')\n\n    return {\n      vocabularyWords,\n      windowTitle,\n      appName,\n      contextText,\n      advancedSettings,\n    }\n  }\n\n  private async getVocabulary(): Promise<string[]> {\n    try {\n      const userId = getCurrentUserId()\n      const dictionaryItems = await DictionaryTable.findAll(userId)\n      return dictionaryItems\n        .filter(item => item.deleted_at === null)\n        .map(item => item.word)\n    } catch (error) {\n      log.error('[ContextGrabber] Error getting vocabulary:', error)\n      return []\n    }\n  }\n\n  private async getWindowContext(): Promise<{\n    windowTitle: string\n    appName: string\n  }> {\n    try {\n      const windowContext = await getActiveWindow()\n      return {\n        windowTitle: windowContext?.title || '',\n        appName: windowContext?.appName || '',\n      }\n    } catch (error) {\n      log.error('[ContextGrabber] Error getting window context:', error)\n      return {\n        windowTitle: '',\n        appName: '',\n      }\n    }\n  }\n\n  private async getContextText(mode: ItoMode): Promise<string> {\n    if (mode !== ItoMode.EDIT) {\n      return ''\n    }\n\n    const { macosAccessibilityContextEnabled } = getAdvancedSettings()\n\n    // Try accessibility API first if enabled\n    if (\n      process.platform === 'darwin' &&\n      macosAccessibilityContextEnabled &&\n      macOSAccessibilityContextProvider.isRunning()\n    ) {\n      try {\n        const result = await timingCollector.timeAsync(\n          TimingEventName.CURSOR_CONTEXT_GATHER,\n          async () =>\n            await macOSAccessibilityContextProvider.getCursorContext({\n              maxCharsBefore: 1000,\n              maxCharsAfter: 1000,\n              timeout: 500,\n              debug: false,\n            }),\n        )\n\n        if (result.success && result.context?.selectedText) {\n          console.log(\n            '[ContextGrabber] Got selected text via accessibility API',\n          )\n          return result.context.selectedText.trim()\n        }\n      } catch (error) {\n        console.log(\n          '[ContextGrabber] Accessibility API failed, falling back to keyboard:',\n          error,\n        )\n      }\n    }\n\n    // Fallback to keyboard-based method\n    console.log('[ContextGrabber] Using keyboard method for selected text')\n    try {\n      const text = await timingCollector.timeAsync(\n        TimingEventName.SELCTED_TEXT_GATHER,\n        async () => await getSelectedTextString(),\n      )\n      console.log('[ContextGrabber] Selected text from keyboard:', text)\n      return text && text.trim().length > 0 ? text : ''\n    } catch (error) {\n      log.error('[ContextGrabber] Error getting context text:', error)\n      return ''\n    }\n  }\n\n  /**\n   * Get cursor context for grammar rules (capitalization, spacing, etc.)\n   * This fetches a small amount of text before the cursor position.\n   *\n   * @param contextLength - Number of characters to fetch before cursor (default: 4)\n   * @returns The text before the cursor, or empty string if unavailable\n   */\n  public async getCursorContextForGrammar(\n    contextLength: number = 4,\n  ): Promise<string> {\n    const { macosAccessibilityContextEnabled } = getAdvancedSettings()\n\n    // Try accessibility API first if enabled\n    if (\n      process.platform === 'darwin' &&\n      macosAccessibilityContextEnabled &&\n      macOSAccessibilityContextProvider.isRunning()\n    ) {\n      try {\n        const result = await macOSAccessibilityContextProvider.getCursorContext(\n          {\n            maxCharsBefore: contextLength,\n            maxCharsAfter: 0,\n            timeout: 500,\n            debug: false,\n          },\n        )\n\n        if (result.success && result.context?.textBefore) {\n          console.log(\n            '[ContextGrabber] Got cursor context via accessibility API',\n          )\n          return result.context.textBefore\n        }\n      } catch (error) {\n        console.log(\n          '[ContextGrabber] Accessibility API failed, falling back to keyboard:',\n          error,\n        )\n      }\n    }\n\n    // Fallback to keyboard-based method\n    console.log('[ContextGrabber] Using keyboard method for cursor context')\n    try {\n      const canGetContext = await canGetContextFromCurrentApp()\n\n      if (!canGetContext) {\n        console.log(\n          '[ContextGrabber] Cannot get cursor context from current app',\n        )\n        return ''\n      }\n\n      const cursorContext = await getCursorContext(contextLength)\n      return cursorContext || ''\n    } catch (error) {\n      log.error(\n        '[ContextGrabber] Error getting cursor context for grammar:',\n        error,\n      )\n      return ''\n    }\n  }\n}\n\nexport const contextGrabber = new ContextGrabber()\n"
  },
  {
    "path": "lib/main/env.ts",
    "content": "import { app } from 'electron'\nimport path from 'path'\n\nlet stage = process.env.ITO_ENV || import.meta.env.VITE_ITO_ENV\nif (!stage && import.meta.env.DEV) {\n  stage = 'local'\n}\nif (!stage) {\n  throw new Error('ITO_ENV or VITE_ITO_ENV must be set to dev or prod')\n}\n\nconst userDataDir = path.join(app.getPath('appData'), `Ito-${stage}`)\napp.setPath('userData', userDataDir)\n\nif (stage !== 'prod') {\n  app.setName(`Ito (${stage})`)\n}\n\nexport const ITO_ENV = stage\n"
  },
  {
    "path": "lib/main/grammar/GrammarRulesService.test.ts",
    "content": "import { describe, test, expect } from 'bun:test'\nimport { GrammarRulesService } from './GrammarRulesService'\n\ndescribe('GrammarRulesService', () => {\n  describe('setCaseFirstWord', () => {\n    describe('Proper Noun Capitalization', () => {\n      test('should always capitalize proper names regardless of context', () => {\n        const grammarService = new GrammarRulesService('hello')\n        const result = grammarService.setCaseFirstWord('john went to the store')\n        expect(result).toBe('John went to the store')\n      })\n\n      test('should always capitalize place names', () => {\n        const grammarService = new GrammarRulesService('hello')\n        const result = grammarService.setCaseFirstWord('california is sunny')\n        expect(result).toBe('California is sunny')\n      })\n\n      test('should always capitalize organization names', () => {\n        const grammarService = new GrammarRulesService('hello')\n        const result = grammarService.setCaseFirstWord(\n          'microsoft released an update',\n        )\n        expect(result).toBe('Microsoft released an update')\n      })\n\n      test('should always capitalize days of the week', () => {\n        const grammarService = new GrammarRulesService('hello')\n        const result = grammarService.setCaseFirstWord('monday is busy')\n        expect(result).toBe('Monday is busy')\n      })\n\n      test('should always capitalize months', () => {\n        const grammarService = new GrammarRulesService('hello')\n        const result = grammarService.setCaseFirstWord('january is cold')\n        expect(result).toBe('January is cold')\n      })\n    })\n\n    describe('Context-based Capitalization', () => {\n      test('should capitalize after sentence endings', () => {\n        const grammarService = new GrammarRulesService('Good morning.')\n        const result = grammarService.setCaseFirstWord('hello world')\n        expect(result).toBe('Hello world')\n      })\n\n      test('should capitalize after exclamation marks', () => {\n        const grammarService = new GrammarRulesService('Great job!')\n        const result = grammarService.setCaseFirstWord('wow that is amazing')\n        expect(result).toBe('Wow that is amazing')\n      })\n\n      test('should capitalize after question marks', () => {\n        const grammarService = new GrammarRulesService('How are you?')\n        const result = grammarService.setCaseFirstWord('yes it is')\n        expect(result).toBe('Yes it is')\n      })\n\n      test('should not capitalize after commas', () => {\n        const grammarService = new GrammarRulesService('We walked,')\n        const result = grammarService.setCaseFirstWord('and then we went')\n        expect(result).toBe('and then we went')\n      })\n\n      test('should not capitalize after semicolons', () => {\n        const grammarService = new GrammarRulesService('We did this;')\n        const result = grammarService.setCaseFirstWord('but first this')\n        expect(result).toBe('but first this')\n      })\n\n      test('should not capitalize mid-sentence', () => {\n        const grammarService = new GrammarRulesService('We went')\n        const result = grammarService.setCaseFirstWord('and then we left')\n        expect(result).toBe('and then we left')\n      })\n\n      test('should capitalize with empty context', () => {\n        const grammarService = new GrammarRulesService('')\n        const result = grammarService.setCaseFirstWord('hello world')\n        expect(result).toBe('Hello world')\n      })\n    })\n\n    describe('Edge Cases', () => {\n      test('should handle multi-word proper nouns', () => {\n        const grammarService = new GrammarRulesService('hello')\n        const result = grammarService.setCaseFirstWord('new york is big')\n        expect(result).toBe('new york is big') // Only first word checked, \"new\" is not a proper noun\n      })\n\n      test('should handle transcript with leading/trailing whitespace', () => {\n        const grammarService = new GrammarRulesService('Hi.')\n        const result = grammarService.setCaseFirstWord('  hello world  ')\n        expect(result).toBe('  Hello world  ')\n      })\n\n      test('should handle context with only whitespace', () => {\n        const grammarService = new GrammarRulesService('   ')\n        const result = grammarService.setCaseFirstWord('hello')\n        expect(result).toBe('Hello')\n      })\n\n      test('should handle very short words', () => {\n        const grammarService = new GrammarRulesService('Hi.')\n        const result = grammarService.setCaseFirstWord('i am here')\n        expect(result).toBe('I am here')\n      })\n\n      test('should handle empty transcript', () => {\n        const grammarService = new GrammarRulesService('Hello world')\n        const result = grammarService.setCaseFirstWord('')\n        expect(result).toBe('')\n      })\n\n      test('should handle transcript with only punctuation', () => {\n        const grammarService = new GrammarRulesService('Hello.')\n        const result = grammarService.setCaseFirstWord('...')\n        expect(result).toBe('...')\n      })\n\n      test('should handle transcript starting with numbers', () => {\n        const grammarService = new GrammarRulesService('Hello.')\n        const result = grammarService.setCaseFirstWord('42 is the answer')\n        expect(result).toBe('42 Is the answer') // Should capitalize first letter found (\"I\" in \"is\")\n      })\n    })\n  })\n\n  describe('addLeadingSpaceIfNeeded', () => {\n    describe('Leading Space Logic', () => {\n      test('should add space after letters', () => {\n        const grammarService = new GrammarRulesService('word')\n        const result = grammarService.addLeadingSpaceIfNeeded('Hello')\n        expect(result).toBe(' Hello')\n      })\n\n      test('should add space after numbers', () => {\n        const grammarService = new GrammarRulesService('123')\n        const result = grammarService.addLeadingSpaceIfNeeded('Hello')\n        expect(result).toBe(' Hello')\n      })\n\n      test('should add space after closing punctuation', () => {\n        const grammarService = new GrammarRulesService('done)')\n        const result = grammarService.addLeadingSpaceIfNeeded('Hello')\n        expect(result).toBe(' Hello')\n      })\n\n      test('should not add space after existing whitespace', () => {\n        const grammarService = new GrammarRulesService('word ')\n        const result = grammarService.addLeadingSpaceIfNeeded('Hello')\n        expect(result).toBe('Hello')\n      })\n\n      test('should not add space after tabs', () => {\n        const grammarService = new GrammarRulesService('word\\t')\n        const result = grammarService.addLeadingSpaceIfNeeded('Hello')\n        expect(result).toBe('Hello')\n      })\n\n      test('should not add space after newlines', () => {\n        const grammarService = new GrammarRulesService('word\\n')\n        const result = grammarService.addLeadingSpaceIfNeeded('Hello')\n        expect(result).toBe('Hello')\n      })\n\n      test('should not add space after opening punctuation', () => {\n        const grammarService = new GrammarRulesService('word(')\n        const result = grammarService.addLeadingSpaceIfNeeded('Hello')\n        expect(result).toBe('Hello')\n      })\n\n      test('should not add space after quotes', () => {\n        const grammarService = new GrammarRulesService('He said \"')\n        const result = grammarService.addLeadingSpaceIfNeeded('Hello')\n        expect(result).toBe('Hello')\n      })\n\n      test('should not add space with empty context', () => {\n        const grammarService = new GrammarRulesService('')\n        const result = grammarService.addLeadingSpaceIfNeeded('Hello')\n        expect(result).toBe('Hello')\n      })\n\n      test('should handle empty transcript', () => {\n        const grammarService = new GrammarRulesService('Hello world')\n        const result = grammarService.addLeadingSpaceIfNeeded('')\n        expect(result).toBe('')\n      })\n    })\n  })\n\n  describe('Combined Usage Examples', () => {\n    test('should capitalize proper noun and add space when used together', () => {\n      const grammarService = new GrammarRulesService('Hi')\n      let result = grammarService.setCaseFirstWord('john is here')\n      result = grammarService.addLeadingSpaceIfNeeded(result)\n      expect(result).toBe(' John is here')\n    })\n\n    test('should capitalize after period and add space when used together', () => {\n      const grammarService = new GrammarRulesService('Done.')\n      let result = grammarService.setCaseFirstWord('this is great')\n      result = grammarService.addLeadingSpaceIfNeeded(result)\n      expect(result).toBe(' This is great')\n    })\n\n    test('should handle proper noun without adding space (after whitespace)', () => {\n      const grammarService = new GrammarRulesService('Hi ')\n      let result = grammarService.setCaseFirstWord('mary called')\n      result = grammarService.addLeadingSpaceIfNeeded(result)\n      expect(result).toBe('Mary called')\n    })\n  })\n})\n"
  },
  {
    "path": "lib/main/grammar/GrammarRulesService.ts",
    "content": "import nlp from 'compromise'\n\nexport class GrammarRulesService {\n  private cursorContext: string = ''\n\n  public constructor(context: string) {\n    this.cursorContext = context\n  }\n\n  /**\n   * Set first word case (uppercase or lowercase) based on stored cursor context\n   */\n  public setCaseFirstWord(transcript: string): string {\n    if (!transcript) return transcript\n\n    // If no cursor context available, just capitalize first letter\n    if (!this.cursorContext) {\n      const firstLetterIndex = transcript.search(/[a-zA-Z]/)\n      if (firstLetterIndex >= 0) {\n        return (\n          transcript.substring(0, firstLetterIndex) +\n          transcript.charAt(firstLetterIndex).toUpperCase() +\n          transcript.substring(firstLetterIndex + 1)\n        )\n      }\n      return transcript\n    }\n\n    let correctedText = transcript\n\n    // Check if we should capitalize the first letter\n    const firstWord = correctedText.trim().split(/\\s+/)[0] || ''\n    const shouldCapitalize = this.shouldCapitalizeBasedOnContext(\n      this.cursorContext,\n      firstWord,\n    )\n\n    const firstLetterIndex = correctedText.search(/[a-zA-Z]/)\n    if (firstLetterIndex < 0) {\n      return correctedText // No letters to capitalize\n    }\n    if (shouldCapitalize) {\n      correctedText =\n        correctedText.substring(0, firstLetterIndex) +\n        correctedText.charAt(firstLetterIndex).toUpperCase() +\n        correctedText.substring(firstLetterIndex + 1)\n    } else {\n      correctedText =\n        correctedText.substring(0, firstLetterIndex) +\n        correctedText.charAt(firstLetterIndex).toLowerCase() +\n        correctedText.substring(firstLetterIndex + 1)\n    }\n\n    return correctedText\n  }\n\n  /**\n   * Add leading space if needed based on stored cursor context\n   */\n  public addLeadingSpaceIfNeeded(transcript: string): string {\n    if (!transcript) return transcript\n\n    // Check if we need to add a space before the text\n    const needsLeadingSpace = this.needsLeadingSpace(this.cursorContext)\n    if (needsLeadingSpace) {\n      return ' ' + transcript\n    }\n\n    return transcript\n  }\n\n  private needsLeadingSpace(context: string): boolean {\n    if (!context || context.length === 0) {\n      return false // No space needed if no context\n    }\n\n    // Don't add space if context already ends with whitespace\n    if (\n      context.endsWith(' ') ||\n      context.endsWith('\\t') ||\n      context.endsWith('\\n')\n    ) {\n      return false\n    }\n\n    const lastChar = context.charAt(context.length - 1)\n\n    // Don't add space after opening punctuation\n    const openingPunctuation = ['(', '[', '{', '\"', \"'\", '`']\n    if (openingPunctuation.includes(lastChar)) {\n      return false\n    }\n\n    // Add space if context ends with a letter, number, or closing punctuation\n    if (/[a-zA-Z0-9)\\]}\"'`.,;:!?]$/.test(context)) {\n      return true\n    }\n\n    // For other cases, do add space\n    return true\n  }\n\n  private isProperNoun(word: string): boolean {\n    if (!word || word.trim().length === 0) return false\n\n    const doc = nlp(word.trim())\n    return (\n      doc.has('#ProperNoun') ||\n      doc.has('#Person') ||\n      doc.has('#Place') ||\n      doc.has('#Organization') ||\n      doc.has('#WeekDay') ||\n      doc.has('#Month')\n    )\n  }\n\n  private shouldCapitalizeBasedOnContext(\n    context: string,\n    firstWord: string,\n  ): boolean {\n    // If the first word is a proper noun, ALWAYS capitalize\n    if (this.isProperNoun(firstWord)) {\n      return true\n    }\n\n    if (!context || context.trim().length === 0) {\n      return true // Default to capitalize if no context\n    }\n\n    const trimmedContext = context.trim()\n\n    // Capitalize after sentence endings (period, exclamation, question mark)\n    const sentenceEndings = ['.', '!', '?']\n    const lastChar = trimmedContext.charAt(trimmedContext.length - 1)\n\n    if (sentenceEndings.includes(lastChar)) {\n      return true\n    }\n\n    // Don't capitalize after commas, semicolons, colons, or within sentences\n    const continuationPunctuation = [',', ';', ':', '-', '–', '—']\n    if (continuationPunctuation.includes(lastChar)) {\n      return false\n    }\n\n    // If context ends with a letter or number, don't capitalize (continuing a sentence)\n    if (/[a-zA-Z0-9]$/.test(trimmedContext)) {\n      return false\n    }\n\n    // Default to capitalize for other cases\n    return true\n  }\n}\n"
  },
  {
    "path": "lib/main/index.d.ts",
    "content": "/// <reference types=\"electron-vite/node\" />\n\ndeclare module '*.css' {\n  const content: string\n  export default content\n}\n\ndeclare module '*.png' {\n  const content: string\n  export default content\n}\n\ndeclare module '*.jpg' {\n  const content: string\n  export default content\n}\n\ndeclare module '*.jpeg' {\n  const content: string\n  export default content\n}\n\ndeclare module '*.svg' {\n  const content: string\n  export default content\n}\n\ndeclare module '*.web' {\n  const content: string\n  export default content\n}\n"
  },
  {
    "path": "lib/main/interactions/InteractionManager.test.ts",
    "content": "import { describe, test, expect, beforeEach, mock } from 'bun:test'\n\n// Mock database utilities\nconst mockDbRun = mock(() => Promise.resolve())\nconst mockDbGet = mock(() => Promise.resolve(undefined))\nconst mockDbAll = mock(() => Promise.resolve([]))\n\nmock.module('../sqlite/utils', () => ({\n  run: mockDbRun,\n  get: mockDbGet,\n  all: mockDbAll,\n}))\n\n// Mock electron-store\nmock.module('electron-store', () => {\n  return {\n    default: class MockStore {\n      get() {\n        return null\n      }\n      set() {}\n      delete() {}\n    },\n  }\n})\n\n// Mock the store\nconst mockMainStore = {\n  get: mock(() => ({ id: 'test-user-123' })),\n}\nmock.module('../store', () => ({\n  default: mockMainStore,\n}))\n\n// Mock electron-log\nmock.module('electron-log', () => ({\n  default: {\n    info: mock(),\n    warn: mock(),\n    error: mock(),\n  },\n}))\n\nimport { InteractionManager } from './InteractionManager'\nimport { STORE_KEYS } from '../../constants/store-keys'\n\ndescribe('InteractionManager', () => {\n  let interactionManager: InteractionManager\n\n  beforeEach(() => {\n    interactionManager = new InteractionManager()\n    mockDbRun.mockClear()\n    mockDbGet.mockClear()\n    mockDbAll.mockClear()\n    mockMainStore.get.mockClear()\n    mockMainStore.get.mockReturnValue({ id: 'test-user-123' })\n  })\n\n  describe('Interaction Lifecycle', () => {\n    test('should start interaction and generate ID', () => {\n      const id = interactionManager.initialize()\n\n      expect(id).toBeDefined()\n      expect(typeof id).toBe('string')\n      expect(id.length).toBeGreaterThan(0)\n      expect(interactionManager.getCurrentInteractionId()).toBe(id)\n    })\n\n    test('should track start time', () => {\n      const beforeStart = Date.now()\n      interactionManager.initialize()\n      const afterStart = Date.now()\n\n      const startTime = interactionManager.getInteractionStartTime()\n      expect(startTime).toBeGreaterThanOrEqual(beforeStart)\n      expect(startTime).toBeLessThanOrEqual(afterStart)\n    })\n\n    test('should clear current interaction', () => {\n      interactionManager.initialize()\n      expect(interactionManager.getCurrentInteractionId()).not.toBeNull()\n\n      interactionManager.clearCurrentInteraction()\n      expect(interactionManager.getCurrentInteractionId()).toBeNull()\n      expect(interactionManager.getInteractionStartTime()).toBeNull()\n    })\n\n    test('should generate unique IDs for different interactions', () => {\n      const id1 = interactionManager.initialize()\n      interactionManager.clearCurrentInteraction()\n      const id2 = interactionManager.initialize()\n\n      expect(id1).not.toBe(id2)\n    })\n  })\n\n  describe('Interaction Creation', () => {\n    test('should create interaction with all data', async () => {\n      const transcript = 'Hello world'\n      const audioBuffer = Buffer.from('audio-data')\n      const sampleRate = 16000\n\n      interactionManager.initialize()\n      await interactionManager.createInteraction(\n        transcript,\n        audioBuffer,\n        sampleRate,\n      )\n\n      expect(mockDbRun).toHaveBeenCalled()\n      // Check the SQL call parameters\n      const call = mockDbRun.mock.calls[0]\n      const sql = call[0] as unknown as string\n      const params = call[1] as unknown as any[]\n\n      expect(sql).toContain('INSERT INTO interactions')\n      expect(params).toContain(interactionManager.getCurrentInteractionId())\n      expect(params).toContain('test-user-123')\n      expect(params).toContain(transcript)\n    })\n\n    test('should skip creation when no current interaction ID', async () => {\n      // Don't start interaction\n      await interactionManager.createInteraction(\n        'test',\n        Buffer.from('audio'),\n        16000,\n      )\n\n      expect(mockDbRun).not.toHaveBeenCalled()\n    })\n\n    test('should skip creation when no user ID', async () => {\n      mockMainStore.get.mockReturnValue(null)\n\n      interactionManager.initialize()\n      await interactionManager.createInteraction(\n        'test',\n        Buffer.from('audio'),\n        16000,\n      )\n\n      expect(mockMainStore.get).toHaveBeenCalledWith(STORE_KEYS.USER_PROFILE)\n      expect(mockDbRun).not.toHaveBeenCalled()\n    })\n  })\n\n  describe('Title Generation', () => {\n    test('should use transcript as title for short transcripts', async () => {\n      const transcript = 'Short message'\n      interactionManager.initialize()\n      await interactionManager.createInteraction(\n        transcript,\n        Buffer.from('audio'),\n        16000,\n      )\n\n      expect(mockDbRun).toHaveBeenCalled()\n      const params = mockDbRun.mock.calls[0][1] as unknown as any[]\n      const titleParam = params[2] // title is at index 2\n      expect(titleParam).toBe(transcript)\n    })\n\n    test('should truncate long transcripts at 50 characters', async () => {\n      const longTranscript =\n        'This is a very long transcript that should be truncated because it exceeds fifty characters'\n\n      interactionManager.initialize()\n      await interactionManager.createInteraction(\n        longTranscript,\n        Buffer.from('audio'),\n        16000,\n      )\n\n      expect(mockDbRun).toHaveBeenCalled()\n      const params = mockDbRun.mock.calls[0][1] as unknown as any[]\n      const titleParam = params[2]\n      expect(titleParam).toBe(\n        'This is a very long transcript that should be trun...',\n      )\n      expect(titleParam.length).toBe(53)\n    })\n\n    test('should use fallback title for empty transcript', async () => {\n      interactionManager.initialize()\n      await interactionManager.createInteraction(\n        '',\n        Buffer.from('audio'),\n        16000,\n      )\n\n      expect(mockDbRun).toHaveBeenCalled()\n      const params = mockDbRun.mock.calls[0][1] as unknown as any[]\n      const titleParam = params[2]\n      expect(titleParam).toBe('Voice interaction')\n    })\n  })\n\n  describe('Duration Calculation', () => {\n    test('should calculate duration from start time', async () => {\n      interactionManager.initialize()\n\n      // Wait a bit to ensure measurable duration\n      await new Promise(resolve => setTimeout(resolve, 10))\n\n      await interactionManager.createInteraction(\n        'test',\n        Buffer.from('audio'),\n        16000,\n      )\n\n      expect(mockDbRun).toHaveBeenCalled()\n      const params = mockDbRun.mock.calls[0][1] as unknown as any[]\n      const durationParam = params[6] // duration_ms is at index 6 in upsert\n      expect(durationParam).toBeGreaterThan(0)\n      expect(durationParam).toBeLessThan(1000) // Should be reasonable\n    })\n\n    test('should handle missing start time', async () => {\n      // Manually set interaction ID without using initialize\n      const manager = new InteractionManager()\n      ;(manager as any).currentInteractionId = 'test-id'\n      ;(manager as any).interactionStartTime = null\n\n      await manager.createInteraction('test', Buffer.from('audio'), 16000)\n\n      expect(mockDbRun).toHaveBeenCalled()\n      const params = mockDbRun.mock.calls[0][1] as unknown as any[]\n      const durationParam = params[6] // duration_ms is at index 6 in upsert\n      expect(durationParam).toBe(0)\n    })\n  })\n\n  describe('Audio Buffer Handling', () => {\n    test('should include audio buffer when not empty', async () => {\n      const audioBuffer = Buffer.from('audio-data')\n\n      interactionManager.initialize()\n      await interactionManager.createInteraction('test', audioBuffer, 16000)\n\n      expect(mockDbRun).toHaveBeenCalled()\n      const params = mockDbRun.mock.calls[0][1] as unknown as any[]\n      const rawAudioParam = params[5] // raw_audio is at index 5\n      expect(rawAudioParam).toEqual(audioBuffer)\n    })\n\n    test('should set null for empty audio buffer', async () => {\n      const emptyBuffer = Buffer.alloc(0)\n\n      interactionManager.initialize()\n      await interactionManager.createInteraction('test', emptyBuffer, 16000)\n\n      expect(mockDbRun).toHaveBeenCalled()\n      const params = mockDbRun.mock.calls[0][1] as unknown as any[]\n      const rawAudioParam = params[5]\n      expect(rawAudioParam).toBeNull()\n    })\n  })\n\n  describe('Error Handling', () => {\n    test('should handle database insertion errors gracefully', async () => {\n      mockDbRun.mockRejectedValueOnce(new Error('Database error'))\n\n      interactionManager.initialize()\n\n      // Should not throw - errors should be caught and logged\n      await expect(\n        interactionManager.createInteraction(\n          'test',\n          Buffer.from('audio'),\n          16000,\n        ),\n      ).resolves.toBeUndefined()\n    })\n  })\n})\n"
  },
  {
    "path": "lib/main/interactions/InteractionManager.ts",
    "content": "import { InteractionsTable } from '../sqlite/repo'\nimport mainStore from '../store'\nimport { STORE_KEYS } from '../../constants/store-keys'\nimport log from 'electron-log'\nimport { v4 as uuidv4 } from 'uuid'\nimport { BrowserWindow } from 'electron'\nimport { timingCollector } from '../timing/TimingCollector'\n\nexport class InteractionManager {\n  private currentInteractionId: string | null = null\n  private interactionStartTime: number | null = null\n\n  initialize(): string {\n    this.currentInteractionId = uuidv4()\n    this.interactionStartTime = Date.now()\n    return this.currentInteractionId\n  }\n\n  getCurrentInteractionId(): string | null {\n    return this.currentInteractionId\n  }\n\n  getInteractionStartTime(): number | null {\n    return this.interactionStartTime\n  }\n\n  adoptInteractionId(id: string) {\n    this.currentInteractionId = id\n    this.interactionStartTime = Date.now()\n  }\n\n  async createInteraction(\n    transcript: string,\n    audioBuffer: Buffer,\n    sampleRate: number,\n    errorMessage?: string,\n    errorCode?: string,\n  ) {\n    if (!this.currentInteractionId) {\n      log.warn(\n        '[InteractionManager] No current interaction ID, skipping interaction creation.',\n      )\n      return\n    }\n\n    try {\n      const userProfile = mainStore.get(STORE_KEYS.USER_PROFILE) as any\n      const userId = userProfile?.id\n\n      if (!userId) {\n        log.warn(\n          '[InteractionManager] No user ID found, not creating interaction.',\n        )\n        return\n      }\n\n      // Calculate interaction duration\n      const interactionEndTime = Date.now()\n      const durationMs = this.interactionStartTime\n        ? interactionEndTime - this.interactionStartTime\n        : 0\n\n      // Create ASR output object with comprehensive information\n      const asrOutput = {\n        transcript,\n        totalAudioBytes: audioBuffer.length,\n        error: errorMessage || null,\n        errorCode: errorCode || null,\n        timestamp: new Date().toISOString(),\n        durationMs,\n      }\n\n      // Generate a meaningful title from the transcript\n      const title =\n        transcript && transcript.length > 50\n          ? transcript.substring(0, 50) + '...'\n          : transcript || 'Voice interaction'\n\n      // Create interaction using upsert to specify our own ID\n      const now = new Date().toISOString()\n      const interactionData = {\n        id: this.currentInteractionId,\n        user_id: userId,\n        title,\n        asr_output: asrOutput,\n        llm_output: errorMessage ? { error: errorMessage } : {},\n        raw_audio: audioBuffer.length > 0 ? audioBuffer : null,\n        raw_audio_id: null,\n        duration_ms: durationMs,\n        sample_rate: sampleRate,\n        created_at: now,\n        updated_at: now,\n        deleted_at: null,\n      }\n\n      await InteractionsTable.upsert(interactionData)\n\n      // Notify all windows about the new interaction\n      BrowserWindow.getAllWindows().forEach(window => {\n        window.webContents.send('interaction-created', {\n          id: this.currentInteractionId,\n          transcript,\n          timestamp: now,\n          durationMs,\n        })\n      })\n    } catch (error) {\n      log.error('[InteractionManager] Failed to create interaction:', error)\n      // Clear timing on error\n      if (this.currentInteractionId) {\n        timingCollector.clearInteraction(this.currentInteractionId)\n      }\n    }\n  }\n\n  clearCurrentInteraction() {\n    this.currentInteractionId = null\n    this.interactionStartTime = null\n  }\n}\n\nexport const interactionManager = new InteractionManager()\n"
  },
  {
    "path": "lib/main/itoSessionManager.test.ts",
    "content": "import { describe, test, expect, beforeEach, mock } from 'bun:test'\nimport { ItoMode } from '@/app/generated/ito_pb'\nimport { createMockTimingCollector } from '../__tests__/setup'\nimport { TimingEventName } from './timing/TimingCollector'\n\nconst mockTimingCollector = createMockTimingCollector()\nmock.module('./timing/TimingCollector', () => ({\n  timingCollector: mockTimingCollector,\n  TimingEventName: TimingEventName,\n}))\n\nconst mockVoiceInputService = {\n  startAudioRecording: mock(() => Promise.resolve()),\n  stopAudioRecording: mock(() => Promise.resolve()),\n}\nmock.module('./voiceInputService', () => ({\n  voiceInputService: mockVoiceInputService,\n}))\n\nconst mockRecordingStateNotifier = {\n  notifyRecordingStarted: mock(),\n  notifyRecordingStopped: mock(),\n  notifyProcessingStarted: mock(),\n  notifyProcessingStopped: mock(),\n}\nmock.module('./recordingStateNotifier', () => ({\n  recordingStateNotifier: mockRecordingStateNotifier,\n}))\n\nconst mockItoStreamController = {\n  initialize: mock(_mode => Promise.resolve(true)),\n  startGrpcStream: mock(() =>\n    Promise.resolve({\n      response: { transcript: 'test transcript' },\n      audioBuffer: Buffer.from('audio-data'),\n      sampleRate: 16000,\n    }),\n  ),\n  setMode: mock(),\n  getCurrentMode: mock(() => ItoMode.TRANSCRIBE),\n  scheduleConfigUpdate: mock(() => Promise.resolve()),\n  getAudioDurationMs: mock(() => 1000),\n  endInteraction: mock(),\n  cancelTranscription: mock(),\n}\nmock.module('./itoStreamController', () => ({\n  itoStreamController: mockItoStreamController,\n}))\n\nconst mockTextInserter = {\n  insertText: mock(() => Promise.resolve(true)),\n}\nmock.module('./text/TextInserter', () => ({\n  TextInserter: class MockTextInserter {\n    insertText = mockTextInserter.insertText\n  },\n}))\n\nconst mockInteractionManager = {\n  getCurrentInteractionId: mock((): string | null => null),\n  adoptInteractionId: mock(),\n  initialize: mock(() => 'test-interaction-123'),\n  createInteraction: mock(() => Promise.resolve()),\n  clearCurrentInteraction: mock(),\n}\nmock.module('./interactions/InteractionManager', () => ({\n  interactionManager: mockInteractionManager,\n}))\n\nconst mockContextGrabber = {\n  gatherContext: mock(() =>\n    Promise.resolve({\n      windowTitle: 'Test Window',\n      appName: 'Test App',\n      contextText: 'Test context',\n      vocabularyWords: ['test', 'word'],\n      advancedSettings: {\n        llm: {\n          asrModel: 'whisper-1',\n          asrProvider: 'openai',\n          asrPrompt: '',\n          noSpeechThreshold: 0.5,\n          llmProvider: 'openai',\n          llmModel: 'gpt-4',\n          llmTemperature: 0.7,\n          transcriptionPrompt: '',\n          editingPrompt: '',\n        },\n        grammarServiceEnabled: false,\n        macosAccessibilityContextEnabled: true,\n      },\n    }),\n  ),\n  getCursorContextForGrammar: mock(() => Promise.resolve('test context')),\n}\nmock.module('./context/ContextGrabber', () => ({\n  contextGrabber: mockContextGrabber,\n}))\n\nconst mockGrammarRulesService = {\n  setCaseFirstWord: mock((text: string) => text),\n  addLeadingSpaceIfNeeded: mock((text: string) => text),\n}\nmock.module('./grammar/GrammarRulesService', () => ({\n  GrammarRulesService: class MockGrammarRulesService {\n    setCaseFirstWord = mockGrammarRulesService.setCaseFirstWord\n    addLeadingSpaceIfNeeded = mockGrammarRulesService.addLeadingSpaceIfNeeded\n  },\n}))\n\nconst mockGetAdvancedSettings = mock(() => ({\n  grammarServiceEnabled: false,\n}))\nmock.module('./store', () => ({\n  getAdvancedSettings: mockGetAdvancedSettings,\n}))\n\nmock.module('electron-log', () => ({\n  default: {\n    info: mock(),\n    warn: mock(),\n    error: mock(),\n  },\n}))\n\nbeforeEach(() => {\n  console.log = mock()\n  console.error = mock()\n})\n\ndescribe('itoSessionManager', () => {\n  beforeEach(() => {\n    // Reset all mocks\n    Object.values(mockVoiceInputService).forEach(mockFn => mockFn.mockClear())\n    Object.values(mockRecordingStateNotifier).forEach(mockFn =>\n      mockFn.mockClear(),\n    )\n    Object.values(mockItoStreamController).forEach(mockFn => mockFn.mockClear())\n    Object.values(mockTextInserter).forEach(mockFn => mockFn.mockClear())\n    Object.values(mockInteractionManager).forEach(mockFn => mockFn.mockClear())\n    Object.values(mockContextGrabber).forEach(mockFn => mockFn.mockClear())\n    Object.values(mockGrammarRulesService).forEach(mockFn => mockFn.mockClear())\n    Object.values(mockTimingCollector).forEach(mockFn => mockFn.mockClear())\n\n    mockGetAdvancedSettings.mockClear()\n\n    // Reset default behaviors\n    mockItoStreamController.initialize.mockResolvedValue(true)\n    mockItoStreamController.startGrpcStream.mockResolvedValue({\n      response: { transcript: 'test transcript' },\n      audioBuffer: Buffer.from('audio-data'),\n      sampleRate: 16000,\n    })\n    mockItoStreamController.getAudioDurationMs.mockReturnValue(1000)\n    mockTextInserter.insertText.mockResolvedValue(true)\n    mockInteractionManager.getCurrentInteractionId.mockReturnValue(null)\n    mockInteractionManager.initialize.mockReturnValue('test-interaction-123')\n    mockGetAdvancedSettings.mockReturnValue({\n      grammarServiceEnabled: false,\n    })\n  })\n\n  test('should start session successfully', async () => {\n    const { ItoSessionManager } = await import('./itoSessionManager')\n    const session = new ItoSessionManager()\n\n    await session.startSession(ItoMode.TRANSCRIBE)\n\n    expect(mockItoStreamController.initialize).toHaveBeenCalledWith(\n      ItoMode.TRANSCRIBE,\n    )\n    expect(mockItoStreamController.startGrpcStream).toHaveBeenCalled()\n    expect(mockItoStreamController.setMode).toHaveBeenCalledWith(\n      ItoMode.TRANSCRIBE,\n    )\n    expect(mockVoiceInputService.startAudioRecording).toHaveBeenCalled()\n    expect(\n      mockRecordingStateNotifier.notifyRecordingStarted,\n    ).toHaveBeenCalledWith(ItoMode.TRANSCRIBE)\n  })\n\n  test('should fetch and send context in background', async () => {\n    const { ItoSessionManager } = await import('./itoSessionManager')\n    const session = new ItoSessionManager()\n\n    await session.startSession(ItoMode.TRANSCRIBE)\n\n    // Wait for background context fetch\n    await new Promise(resolve => setTimeout(resolve, 50))\n\n    expect(mockItoStreamController.scheduleConfigUpdate).toHaveBeenCalled()\n  })\n\n  test('should fetch cursor context when grammar is enabled', async () => {\n    mockGetAdvancedSettings.mockReturnValue({\n      grammarServiceEnabled: true,\n    })\n\n    const { ItoSessionManager } = await import('./itoSessionManager')\n    const session = new ItoSessionManager()\n\n    await session.startSession(ItoMode.TRANSCRIBE)\n\n    // Wait for background context fetch\n    await new Promise(resolve => setTimeout(resolve, 60))\n\n    expect(mockContextGrabber.getCursorContextForGrammar).toHaveBeenCalledTimes(\n      1,\n    )\n    expect(mockContextGrabber.getCursorContextForGrammar).toHaveBeenCalled()\n  })\n\n  test('should not fetch cursor context when grammar is disabled', async () => {\n    mockGetAdvancedSettings.mockReturnValue({\n      grammarServiceEnabled: false,\n    })\n\n    const { ItoSessionManager } = await import('./itoSessionManager')\n    const session = new ItoSessionManager()\n\n    await session.startSession(ItoMode.TRANSCRIBE)\n\n    // Wait for background context fetch\n    await new Promise(resolve => setTimeout(resolve, 50))\n  })\n\n  test('should fail to start session when controller fails', async () => {\n    mockItoStreamController.initialize.mockResolvedValueOnce(false)\n\n    const { ItoSessionManager } = await import('./itoSessionManager')\n    const session = new ItoSessionManager()\n\n    await session.startSession(ItoMode.TRANSCRIBE)\n\n    expect(mockVoiceInputService.startAudioRecording).not.toHaveBeenCalled()\n  })\n\n  test('should change mode during session', async () => {\n    const { ItoSessionManager } = await import('./itoSessionManager')\n    const session = new ItoSessionManager()\n\n    session.setMode(ItoMode.EDIT)\n\n    expect(mockItoStreamController.setMode).toHaveBeenCalledWith(ItoMode.EDIT)\n    expect(\n      mockRecordingStateNotifier.notifyRecordingStarted,\n    ).toHaveBeenCalledWith(ItoMode.EDIT)\n  })\n\n  test('should cancel session successfully', async () => {\n    const { ItoSessionManager } = await import('./itoSessionManager')\n    const session = new ItoSessionManager()\n\n    await session.cancelSession()\n\n    expect(mockItoStreamController.cancelTranscription).toHaveBeenCalled()\n    expect(mockVoiceInputService.stopAudioRecording).toHaveBeenCalled()\n    expect(mockRecordingStateNotifier.notifyRecordingStopped).toHaveBeenCalled()\n  })\n\n  test('should complete session with sufficient audio', async () => {\n    const { ItoSessionManager } = await import('./itoSessionManager')\n    const session = new ItoSessionManager()\n\n    mockItoStreamController.getAudioDurationMs.mockReturnValue(500)\n\n    await session.startSession(ItoMode.TRANSCRIBE)\n    await session.completeSession()\n\n    expect(mockVoiceInputService.stopAudioRecording).toHaveBeenCalled()\n    expect(mockItoStreamController.endInteraction).toHaveBeenCalled()\n    expect(mockRecordingStateNotifier.notifyRecordingStopped).toHaveBeenCalled()\n  })\n\n  test('should cancel session when audio too short', async () => {\n    const { ItoSessionManager } = await import('./itoSessionManager')\n    const session = new ItoSessionManager()\n\n    mockItoStreamController.getAudioDurationMs.mockReturnValue(50)\n\n    await session.startSession(ItoMode.TRANSCRIBE)\n    await session.completeSession()\n\n    expect(mockItoStreamController.cancelTranscription).toHaveBeenCalled()\n    expect(mockItoStreamController.endInteraction).not.toHaveBeenCalled()\n    expect(mockRecordingStateNotifier.notifyRecordingStopped).toHaveBeenCalled()\n  })\n\n  test('should handle successful transcription response', async () => {\n    const mockTranscript = 'Hello world'\n    mockItoStreamController.startGrpcStream.mockResolvedValueOnce({\n      response: { transcript: mockTranscript },\n      audioBuffer: Buffer.from('audio-data'),\n      sampleRate: 16000,\n    })\n\n    const { ItoSessionManager } = await import('./itoSessionManager')\n    const session = new ItoSessionManager()\n\n    await session.startSession(ItoMode.TRANSCRIBE)\n    await session.completeSession()\n\n    expect(mockTextInserter.insertText).toHaveBeenCalledWith(mockTranscript)\n    expect(mockInteractionManager.createInteraction).toHaveBeenCalledWith(\n      mockTranscript,\n      Buffer.from('audio-data'),\n      16000,\n      undefined,\n    )\n    expect(mockItoStreamController.endInteraction).toHaveBeenCalled()\n    expect(mockInteractionManager.clearCurrentInteraction).toHaveBeenCalled()\n  })\n\n  test('should apply grammar rules when enabled', async () => {\n    mockGetAdvancedSettings.mockReturnValue({\n      grammarServiceEnabled: true,\n    })\n\n    const mockTranscript = 'hello world'\n    mockItoStreamController.startGrpcStream.mockResolvedValueOnce({\n      response: { transcript: mockTranscript },\n      audioBuffer: Buffer.from('audio-data'),\n      sampleRate: 16000,\n    })\n\n    mockGrammarRulesService.setCaseFirstWord.mockReturnValue('Hello world')\n    mockGrammarRulesService.addLeadingSpaceIfNeeded.mockReturnValue(\n      ' Hello world',\n    )\n\n    const { ItoSessionManager } = await import('./itoSessionManager')\n    const session = new ItoSessionManager()\n\n    await session.startSession(ItoMode.TRANSCRIBE)\n    // Allow background context fetch to set up grammarRulesService\n    await new Promise(resolve => setTimeout(resolve, 60))\n    await session.completeSession()\n\n    expect(mockGrammarRulesService.setCaseFirstWord).toHaveBeenCalledWith(\n      mockTranscript,\n    )\n    expect(\n      mockGrammarRulesService.addLeadingSpaceIfNeeded,\n    ).toHaveBeenCalledWith('Hello world')\n    expect(mockTextInserter.insertText).toHaveBeenCalledWith(' Hello world')\n  })\n\n  test('should not apply grammar rules when disabled', async () => {\n    mockGetAdvancedSettings.mockReturnValue({\n      grammarServiceEnabled: false,\n    })\n\n    const mockTranscript = 'hello world'\n    mockItoStreamController.startGrpcStream.mockResolvedValueOnce({\n      response: { transcript: mockTranscript },\n      audioBuffer: Buffer.from('audio-data'),\n      sampleRate: 16000,\n    })\n\n    const { ItoSessionManager } = await import('./itoSessionManager')\n    const session = new ItoSessionManager()\n\n    await session.startSession(ItoMode.TRANSCRIBE)\n    await session.completeSession()\n\n    expect(mockGrammarRulesService.setCaseFirstWord).not.toHaveBeenCalled()\n    expect(\n      mockGrammarRulesService.addLeadingSpaceIfNeeded,\n    ).not.toHaveBeenCalled()\n    expect(mockTextInserter.insertText).toHaveBeenCalledWith(mockTranscript)\n  })\n\n  test('should handle transcription error from server', async () => {\n    const errorMessage = 'ASR service unavailable'\n    mockItoStreamController.startGrpcStream.mockResolvedValueOnce({\n      response: {\n        transcript: '',\n        error: { message: errorMessage },\n      } as any,\n      audioBuffer: Buffer.from('audio-data'),\n      sampleRate: 16000,\n    })\n\n    const { ItoSessionManager } = await import('./itoSessionManager')\n    const session = new ItoSessionManager()\n\n    await session.startSession(ItoMode.TRANSCRIBE)\n    await session.completeSession()\n\n    expect(mockTextInserter.insertText).not.toHaveBeenCalled()\n    expect(mockInteractionManager.createInteraction).toHaveBeenCalledWith(\n      '',\n      Buffer.from('audio-data'),\n      16000,\n      errorMessage,\n    )\n    expect(mockItoStreamController.endInteraction).toHaveBeenCalled()\n    expect(mockInteractionManager.clearCurrentInteraction).toHaveBeenCalled()\n  })\n\n  test('should handle unexpected transcription error', async () => {\n    const error = new Error('Network timeout')\n    mockItoStreamController.startGrpcStream.mockRejectedValueOnce(error)\n\n    const { ItoSessionManager } = await import('./itoSessionManager')\n    const session = new ItoSessionManager()\n\n    await session.startSession(ItoMode.TRANSCRIBE)\n    await session.completeSession()\n\n    expect(mockItoStreamController.endInteraction).toHaveBeenCalled()\n    expect(mockInteractionManager.clearCurrentInteraction).toHaveBeenCalled()\n  })\n\n  test('should skip text insertion when no transcript', async () => {\n    mockItoStreamController.startGrpcStream.mockResolvedValueOnce({\n      response: { transcript: '' },\n      audioBuffer: Buffer.from('audio-data'),\n      sampleRate: 16000,\n    })\n\n    const { ItoSessionManager } = await import('./itoSessionManager')\n    const session = new ItoSessionManager()\n\n    await session.startSession(ItoMode.TRANSCRIBE)\n    await session.completeSession()\n\n    expect(mockTextInserter.insertText).not.toHaveBeenCalled()\n  })\n\n  test('should handle context fetch error gracefully', async () => {\n    mockItoStreamController.scheduleConfigUpdate.mockRejectedValueOnce(\n      new Error('Context fetch failed'),\n    )\n\n    const { ItoSessionManager } = await import('./itoSessionManager')\n    const session = new ItoSessionManager()\n\n    // Should not throw\n    await session.startSession(ItoMode.TRANSCRIBE)\n\n    // Wait for background context fetch to complete\n    await new Promise(resolve => setTimeout(resolve, 50))\n\n    // Session should still continue normally\n    expect(mockVoiceInputService.startAudioRecording).toHaveBeenCalled()\n  })\n\n  test('should handle complete session flow', async () => {\n    const { ItoSessionManager } = await import('./itoSessionManager')\n    const session = new ItoSessionManager()\n\n    const mockTranscript = 'Test complete flow'\n    mockItoStreamController.startGrpcStream.mockResolvedValueOnce({\n      response: { transcript: mockTranscript },\n      audioBuffer: Buffer.from('audio-data'),\n      sampleRate: 16000,\n    })\n\n    // Start session\n    await session.startSession(ItoMode.TRANSCRIBE)\n\n    expect(mockItoStreamController.initialize).toHaveBeenCalled()\n    expect(mockVoiceInputService.startAudioRecording).toHaveBeenCalled()\n\n    // Complete session\n    await session.completeSession()\n\n    expect(mockVoiceInputService.stopAudioRecording).toHaveBeenCalled()\n    expect(mockItoStreamController.endInteraction).toHaveBeenCalled()\n    expect(mockTextInserter.insertText).toHaveBeenCalledWith(mockTranscript)\n    expect(mockRecordingStateNotifier.notifyRecordingStopped).toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "lib/main/itoSessionManager.ts",
    "content": "import { ItoMode } from '@/app/generated/ito_pb'\nimport { voiceInputService } from './voiceInputService'\nimport { recordingStateNotifier } from './recordingStateNotifier'\nimport { itoStreamController } from './itoStreamController'\nimport { TextInserter } from './text/TextInserter'\nimport { interactionManager } from './interactions/InteractionManager'\nimport { contextGrabber } from './context/ContextGrabber'\nimport { GrammarRulesService } from './grammar/GrammarRulesService'\nimport { getAdvancedSettings } from './store'\nimport log from 'electron-log'\nimport { timingCollector, TimingEventName } from './timing/TimingCollector'\n\nexport class ItoSessionManager {\n  private readonly MINIMUM_AUDIO_DURATION_MS = 100\n  private textInserter = new TextInserter()\n  private streamResponsePromise: Promise<{\n    response: any\n    audioBuffer: Buffer\n    sampleRate: number\n  }> | null = null\n  private grammarRulesService = new GrammarRulesService('')\n\n  public async startSession(mode: ItoMode) {\n    console.log('[itoSessionManager] Starting session with mode:', mode)\n\n    // Reuse existing global interaction ID if present, otherwise create a new one\n    let interactionId = interactionManager.getCurrentInteractionId()\n    if (interactionId) {\n      console.log(\n        '[itoSessionManager] Reusing existing interaction ID:',\n        interactionId,\n      )\n      interactionManager.adoptInteractionId(interactionId)\n    } else {\n      interactionId = interactionManager.initialize()\n    }\n\n    // Initialize all necessary components\n    const started = await itoStreamController.initialize(mode)\n    if (!started) {\n      log.error('[itoSessionManager] Failed to initialize itoStreamController')\n      return\n    }\n\n    // Begin gRPC stream immediately (note, no audio is flowing yet)\n    this.streamResponsePromise = itoStreamController.startGrpcStream()\n\n    // Begin recording audio (audio bytes will now flow into the gRPC stream)\n    voiceInputService.startAudioRecording()\n\n    // Send initial mode to the stream\n    itoStreamController.setMode(mode)\n\n    // Update UI state\n    recordingStateNotifier.notifyRecordingStarted(mode)\n\n    // Fetch and send context in the background (non-blocking)\n    this.fetchAndSendContext().catch(error => {\n      log.error('[itoSessionManager] Failed to fetch/send context:', error)\n    })\n\n    // Start timing the interaction\n    timingCollector.startInteraction()\n    timingCollector.startTiming(TimingEventName.INTERACTION_ACTIVE)\n\n    return interactionId\n  }\n\n  private async fetchAndSendContext() {\n    console.log('[itoSessionManager] Gathering context...')\n\n    // Gather all context data (window, app, selected text, vocabulary, settings)\n    const context = await contextGrabber.gatherContext(\n      itoStreamController.getCurrentMode(),\n    )\n\n    // Send the gathered context to the stream controller\n    await itoStreamController.scheduleConfigUpdate(context)\n\n    // Fetch cursor context for grammar rules only if grammar service is enabled\n    const { grammarServiceEnabled } = getAdvancedSettings()\n    if (grammarServiceEnabled) {\n      const cursorContext = await timingCollector.timeAsync(\n        TimingEventName.GRAMMAR_SERVICE,\n        async () => await contextGrabber.getCursorContextForGrammar(),\n      )\n      this.grammarRulesService = new GrammarRulesService(cursorContext)\n    }\n  }\n\n  public setMode(mode: ItoMode) {\n    // Send mode change to grpc stream (will also update windows via recordingStateNotifier)\n    itoStreamController.setMode(mode)\n\n    // Update UI to show the new mode\n    recordingStateNotifier.notifyRecordingStarted(mode)\n  }\n\n  public async cancelSession() {\n    // Capture the promise in a local variable immediately so new sessions can start\n    const responsePromise = this.streamResponsePromise\n    this.streamResponsePromise = null\n\n    // Clear timing for the interaction on cancel\n    timingCollector.clearInteraction()\n\n    // Cancel the transcription (will not create interaction)\n    itoStreamController.cancelTranscription()\n    interactionManager.clearCurrentInteraction()\n\n    // Stop audio recording\n    await voiceInputService.stopAudioRecording()\n\n    // Update UI state\n    recordingStateNotifier.notifyRecordingStopped()\n\n    // Wait for the stream promise to reject with cancellation error\n    if (responsePromise) {\n      try {\n        await responsePromise\n      } catch (error) {\n        // Expected cancellation error, log and ignore\n        console.log('[itoSessionManager] Stream cancelled as expected:', error)\n      }\n    }\n  }\n\n  public async completeSession() {\n    // Capture the promise in a local variable immediately so new sessions can start\n    const responsePromise = this.streamResponsePromise\n    this.streamResponsePromise = null\n\n    // End timing for the interaction\n    timingCollector.endTiming(TimingEventName.INTERACTION_ACTIVE)\n\n    // Stop audio recording and wait for drain\n    await voiceInputService.stopAudioRecording()\n\n    // Check actual audio duration (keyboard duration can be misleading due to latency)\n    const audioDurationMs = itoStreamController.getAudioDurationMs()\n\n    if (audioDurationMs < this.MINIMUM_AUDIO_DURATION_MS) {\n      console.log(\n        `[itoSessionManager] Audio too short (${audioDurationMs}ms < ${this.MINIMUM_AUDIO_DURATION_MS}ms), cancelling`,\n      )\n      itoStreamController.cancelTranscription()\n      recordingStateNotifier.notifyRecordingStopped()\n\n      // Wait for the stream promise to reject with cancellation error\n      if (responsePromise) {\n        try {\n          await responsePromise\n        } catch (error) {\n          // Expected cancellation error, log and ignore\n          console.log(\n            '[itoSessionManager] Stream cancelled as expected:',\n            error,\n          )\n        }\n      }\n      return\n    }\n\n    // End the interaction (this will complete the gRPC stream)\n    itoStreamController.endInteraction()\n\n    // Update UI state\n    recordingStateNotifier.notifyRecordingStopped()\n\n    // Notify processing started\n    recordingStateNotifier.notifyProcessingStarted()\n\n    // Wait for the stream response and handle it\n    if (responsePromise) {\n      console.log(\n        '[itoSessionManager] Waiting for stream response from server...',\n      )\n      try {\n        const result = await responsePromise\n        console.log('[itoSessionManager] Received stream response:', {\n          hasTranscript: !!result.response?.transcript,\n          transcriptLength: result.response?.transcript?.length || 0,\n          hasError: !!result.response?.error,\n          audioBufferSize: result.audioBuffer.length,\n        })\n        await this.handleTranscriptionResponse(result)\n      } catch (error) {\n        console.error(\n          '[itoSessionManager] Error waiting for stream response:',\n          error,\n        )\n        await this.handleTranscriptionError(error)\n      } finally {\n        // Always notify processing stopped after handling response\n        recordingStateNotifier.notifyProcessingStopped()\n      }\n    } else {\n      console.warn('[itoSessionManager] No stream response promise to wait for')\n      recordingStateNotifier.notifyProcessingStopped()\n    }\n  }\n\n  private async handleTranscriptionResponse(result: {\n    response: any\n    audioBuffer: Buffer\n    sampleRate: number\n  }) {\n    const { response, audioBuffer, sampleRate } = result\n\n    const errorMessage = response.error ? response.error.message : undefined\n\n    // Handle any transcription error\n    if (response.error) {\n      await interactionManager.createInteraction(\n        response.transcript || '',\n        audioBuffer,\n        sampleRate,\n        errorMessage,\n      )\n      timingCollector.clearInteraction()\n      interactionManager.clearCurrentInteraction()\n    } else {\n      // Handle text insertion with grammar-corrected text\n      if (response.transcript && !response.error) {\n        let textToInsert = response.transcript\n\n        // Apply grammar rules only if grammar service is enabled\n        const { grammarServiceEnabled } = getAdvancedSettings()\n        if (grammarServiceEnabled) {\n          textToInsert = this.grammarRulesService.setCaseFirstWord(textToInsert)\n          textToInsert =\n            this.grammarRulesService.addLeadingSpaceIfNeeded(textToInsert)\n        }\n\n        this.textInserter.insertText(textToInsert)\n\n        // Create interaction in database\n        await interactionManager.createInteraction(\n          response.transcript,\n          audioBuffer,\n          sampleRate,\n          errorMessage,\n        )\n      } else {\n        log.warn('[itoSessionManager] Skipping text insertion:', {\n          hasTranscript: !!response.transcript,\n          transcriptLength: response.transcript?.length || 0,\n          hasError: !!response.error,\n        })\n      }\n      timingCollector.finalizeInteraction()\n      interactionManager.clearCurrentInteraction()\n      itoStreamController.clearInteractionAudio()\n    }\n  }\n\n  private async handleTranscriptionError(error: any) {\n    log.error(\n      '[itoSessionManager] An unexpected error occurred during transcription:',\n      error,\n    )\n    // Clear timing for the interaction on error\n    timingCollector.clearInteraction()\n\n    // Clear current interaction on error\n    interactionManager.clearCurrentInteraction()\n  }\n}\n\nexport const itoSessionManager = new ItoSessionManager()\n"
  },
  {
    "path": "lib/main/itoStreamController.test.ts",
    "content": "import { describe, test, expect, beforeEach, mock } from 'bun:test'\nimport { ItoMode } from '@/app/generated/ito_pb'\n\nconst mockGrpcClient = {\n  transcribeStreamV2: mock(() =>\n    Promise.resolve({ transcript: 'default' } as any),\n  ),\n}\nmock.module('../clients/grpcClient', () => ({\n  grpcClient: mockGrpcClient,\n}))\n\nconst mockAudioStreamManager = {\n  isCurrentlyStreaming: mock(() => false),\n  initialize: mock(),\n  stopStreaming: mock(),\n  addAudioChunk: mock(),\n  setAudioConfig: mock(),\n  getInteractionAudioBuffer: mock(() => Buffer.from('audio-data')),\n  getCurrentSampleRate: mock(() => 16000),\n  clearInteractionAudio: mock(),\n  getAudioDurationMs: mock(() => 1000),\n  streamAudioChunks: mock(\n    () =>\n      async function* () {\n        yield { audioData: Buffer.from('test-chunk-1') }\n        yield { audioData: Buffer.from('test-chunk-2') }\n      },\n  ),\n}\nmock.module('./audio/AudioStreamManager', () => ({\n  AudioStreamManager: class MockAudioStreamManager {\n    isCurrentlyStreaming = mockAudioStreamManager.isCurrentlyStreaming\n    initialize = mockAudioStreamManager.initialize\n    stopStreaming = mockAudioStreamManager.stopStreaming\n    addAudioChunk = mockAudioStreamManager.addAudioChunk\n    setAudioConfig = mockAudioStreamManager.setAudioConfig\n    getInteractionAudioBuffer = mockAudioStreamManager.getInteractionAudioBuffer\n    getCurrentSampleRate = mockAudioStreamManager.getCurrentSampleRate\n    clearInteractionAudio = mockAudioStreamManager.clearInteractionAudio\n    getAudioDurationMs = mockAudioStreamManager.getAudioDurationMs\n    streamAudioChunks = mockAudioStreamManager.streamAudioChunks\n  },\n}))\n\nconst mockContextGrabber = {\n  gatherContext: mock(() =>\n    Promise.resolve({\n      windowTitle: 'Test Window',\n      appName: 'Test App',\n      contextText: 'Test context',\n      vocabularyWords: ['test', 'word'],\n      advancedSettings: {\n        llm: {\n          asrModel: 'whisper-1',\n          asrProvider: 'openai',\n          asrPrompt: '',\n          noSpeechThreshold: 0.5,\n          llmProvider: 'openai',\n          llmModel: 'gpt-4',\n          llmTemperature: 0.7,\n          transcriptionPrompt: '',\n          editingPrompt: '',\n        },\n        grammarServiceEnabled: false,\n        macosAccessibilityContextEnabled: true,\n      },\n    }),\n  ),\n}\nmock.module('./context/ContextGrabber', () => ({\n  contextGrabber: mockContextGrabber,\n}))\n\nmock.module('electron-log', () => ({\n  default: {\n    info: mock(),\n    warn: mock(),\n    error: mock(),\n  },\n}))\n\nbeforeEach(() => {\n  console.log = mock()\n  console.error = mock()\n})\n\ndescribe('ItoStreamController', () => {\n  beforeEach(() => {\n    // Reset all mocks\n    Object.values(mockAudioStreamManager).forEach(mockFn => mockFn.mockClear())\n    Object.values(mockContextGrabber).forEach(mockFn => mockFn.mockClear())\n\n    mockGrpcClient.transcribeStreamV2.mockClear()\n    mockGrpcClient.transcribeStreamV2.mockResolvedValue({\n      transcript: 'default',\n    })\n\n    // Reset default behaviors\n    mockAudioStreamManager.isCurrentlyStreaming.mockReturnValue(false)\n    mockAudioStreamManager.getAudioDurationMs.mockReturnValue(1000)\n    mockAudioStreamManager.getInteractionAudioBuffer.mockReturnValue(\n      Buffer.from('audio-data'),\n    )\n    mockAudioStreamManager.getCurrentSampleRate.mockReturnValue(16000)\n  })\n\n  test('should start interaction successfully', async () => {\n    const { ItoStreamController } = await import('./itoStreamController')\n    const controller = new ItoStreamController()\n\n    const started = await controller.initialize(ItoMode.TRANSCRIBE)\n\n    expect(started).toBe(true)\n    expect(mockAudioStreamManager.initialize).toHaveBeenCalled()\n  })\n\n  test('should prevent multiple concurrent interactions', async () => {\n    const { ItoStreamController } = await import('./itoStreamController')\n    const controller = new ItoStreamController()\n\n    mockAudioStreamManager.isCurrentlyStreaming.mockReturnValue(true)\n\n    const started = await controller.initialize(ItoMode.TRANSCRIBE)\n\n    expect(started).toBe(false)\n  })\n\n  test('should start gRPC stream successfully', async () => {\n    const { ItoStreamController } = await import('./itoStreamController')\n    const controller = new ItoStreamController()\n\n    const mockResponse = {\n      transcript: 'Hello world',\n      audio: Buffer.from('audio'),\n    }\n    mockGrpcClient.transcribeStreamV2.mockResolvedValueOnce(mockResponse)\n\n    await controller.initialize(ItoMode.TRANSCRIBE)\n\n    const result = await controller.startGrpcStream()\n\n    expect(mockGrpcClient.transcribeStreamV2).toHaveBeenCalled()\n    expect(result).toEqual({\n      response: mockResponse,\n      audioBuffer: Buffer.from('audio-data'),\n      sampleRate: 16000,\n    })\n  })\n\n  test('should throw error when starting gRPC stream twice', async () => {\n    const { ItoStreamController } = await import('./itoStreamController')\n    const controller = new ItoStreamController()\n\n    await controller.initialize(ItoMode.TRANSCRIBE)\n    await controller.startGrpcStream()\n\n    await expect(controller.startGrpcStream()).rejects.toThrow(\n      'Stream already started',\n    )\n  })\n\n  test('should change mode during streaming', async () => {\n    const { ItoStreamController } = await import('./itoStreamController')\n    const controller = new ItoStreamController()\n\n    mockAudioStreamManager.isCurrentlyStreaming.mockReturnValue(true)\n\n    controller.setMode(ItoMode.EDIT)\n\n    // Mode change should be queued - we can't easily verify the queue directly,\n    // but we can verify it doesn't throw and the warning isn't logged for inactive stream\n    expect(mockAudioStreamManager.isCurrentlyStreaming).toHaveBeenCalled()\n  })\n\n  test('should warn when changing mode without active stream', async () => {\n    const { ItoStreamController } = await import('./itoStreamController')\n    const controller = new ItoStreamController()\n\n    mockAudioStreamManager.isCurrentlyStreaming.mockReturnValue(false)\n\n    controller.setMode(ItoMode.EDIT)\n\n    expect(mockAudioStreamManager.isCurrentlyStreaming).toHaveBeenCalled()\n  })\n\n  test('should send config update during streaming', async () => {\n    const { ItoStreamController } = await import('./itoStreamController')\n    const controller = new ItoStreamController()\n\n    await controller.initialize(ItoMode.TRANSCRIBE)\n    mockAudioStreamManager.isCurrentlyStreaming.mockReturnValue(true)\n\n    const mockContext = await mockContextGrabber.gatherContext()\n    await controller.scheduleConfigUpdate(mockContext)\n\n    expect(mockContextGrabber.gatherContext).toHaveBeenCalled()\n  })\n\n  test('should warn when sending config without active stream', async () => {\n    const { ItoStreamController } = await import('./itoStreamController')\n    const controller = new ItoStreamController()\n\n    mockAudioStreamManager.isCurrentlyStreaming.mockReturnValue(false)\n\n    const mockContext = await mockContextGrabber.gatherContext()\n    await controller.scheduleConfigUpdate(mockContext)\n\n    // Should not be called again since we already called it to get mockContext\n    expect(mockContextGrabber.gatherContext).toHaveBeenCalledTimes(1)\n  })\n\n  test('should end interaction successfully', async () => {\n    const { ItoStreamController } = await import('./itoStreamController')\n    const controller = new ItoStreamController()\n\n    mockAudioStreamManager.isCurrentlyStreaming.mockReturnValue(true)\n\n    controller.endInteraction()\n\n    expect(mockAudioStreamManager.stopStreaming).toHaveBeenCalled()\n  })\n\n  test('should warn when ending non-existent interaction', async () => {\n    const { ItoStreamController } = await import('./itoStreamController')\n    const controller = new ItoStreamController()\n\n    mockAudioStreamManager.isCurrentlyStreaming.mockReturnValue(false)\n\n    controller.endInteraction()\n\n    expect(mockAudioStreamManager.stopStreaming).not.toHaveBeenCalled()\n  })\n\n  test('should cancel transcription successfully', async () => {\n    const { ItoStreamController } = await import('./itoStreamController')\n    const controller = new ItoStreamController()\n\n    mockAudioStreamManager.isCurrentlyStreaming.mockReturnValue(true)\n    await controller.initialize(ItoMode.TRANSCRIBE)\n\n    controller.cancelTranscription()\n\n    expect(mockAudioStreamManager.stopStreaming).toHaveBeenCalled()\n  })\n\n  test('should return audio duration', async () => {\n    const { ItoStreamController } = await import('./itoStreamController')\n    const controller = new ItoStreamController()\n\n    mockAudioStreamManager.getAudioDurationMs.mockReturnValue(5000)\n\n    const duration = controller.getAudioDurationMs()\n\n    expect(duration).toBe(5000)\n    expect(mockAudioStreamManager.getAudioDurationMs).toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "lib/main/itoStreamController.ts",
    "content": "import {\n  ItoMode,\n  TranscribeStreamRequest,\n  TranscribeStreamRequestSchema,\n  StreamConfigSchema,\n  ContextInfoSchema,\n  LlmSettingsSchema,\n} from '@/app/generated/ito_pb'\nimport { create } from '@bufbuild/protobuf'\nimport { grpcClient } from '../clients/grpcClient'\nimport { AudioStreamManager } from './audio/AudioStreamManager'\nimport { ContextData } from './context/ContextGrabber'\nimport log from 'electron-log'\nimport { timingCollector, TimingEventName } from './timing/TimingCollector'\nimport { interactionManager } from './interactions/InteractionManager'\n\n/**\n * ItoStreamController manages the lifecycle of a transcription stream using TranscribeStreamV2.\n * It allows sending metadata/config, streaming audio, and updating settings during the stream.\n */\nexport class ItoStreamController {\n  private audioStreamManager = new AudioStreamManager()\n\n  private hasStartedGrpc = false\n  private currentMode: ItoMode = ItoMode.TRANSCRIBE\n  private isCancelled = false\n  private configQueue: TranscribeStreamRequest[] = []\n  private abortController: AbortController | null = null\n\n  public async initialize(mode: ItoMode): Promise<boolean> {\n    // Guard against multiple concurrent transcriptions\n    if (this.audioStreamManager.isCurrentlyStreaming()) {\n      log.warn('[ItoStreamController] Stream already in progress.')\n      return false\n    }\n\n    this.audioStreamManager.initialize()\n    this.hasStartedGrpc = false\n    this.currentMode = mode\n    this.isCancelled = false\n    this.configQueue = []\n    this.abortController = null\n    console.log('[ItoStreamController] Starting new interaction stream.')\n\n    return true\n  }\n\n  /**\n   * Starts the gRPC stream immediately without waiting for minimum audio duration.\n   * Returns a promise that resolves with the transcription response and audio data.\n   */\n  public async startGrpcStream(): Promise<{\n    response: any\n    audioBuffer: Buffer\n    sampleRate: number\n  }> {\n    if (this.hasStartedGrpc) {\n      log.warn('[ItoStreamController] gRPC stream already started')\n      throw new Error('Stream already started')\n    }\n\n    console.log('[ItoStreamController] Starting gRPC stream immediately')\n    this.hasStartedGrpc = true\n    this.abortController = new AbortController()\n    const abortSignal = this.abortController.signal\n    const timingEventName =\n      this.currentMode === ItoMode.EDIT\n        ? TimingEventName.SERVER_EDITING\n        : TimingEventName.SERVER_DICTATION\n\n    const response = await timingCollector.timeAsync(\n      timingEventName,\n      async () =>\n        await grpcClient.transcribeStreamV2(\n          this.createStreamGenerator(),\n          abortSignal,\n        ),\n    )\n\n    // Return response along with the audio data collected during the stream\n    return {\n      response,\n      audioBuffer: this.audioStreamManager.getInteractionAudioBuffer(),\n      sampleRate: this.audioStreamManager.getCurrentSampleRate(),\n    }\n  }\n\n  public getCurrentMode(): ItoMode {\n    return this.currentMode\n  }\n\n  public setMode(mode: ItoMode) {\n    if (!this.audioStreamManager.isCurrentlyStreaming()) {\n      log.warn('[ItoStreamController] Cannot change mode - no active stream')\n      return\n    }\n\n    this.currentMode = mode\n    console.log(`[ItoStreamController] Mode changed to ${mode}`)\n\n    // Send mode update to stream\n    this.sendModeUpdate(mode)\n  }\n\n  public scheduleConfigUpdate(context: ContextData) {\n    if (!this.audioStreamManager.isCurrentlyStreaming()) {\n      log.warn('[ItoStreamController] Cannot send config - no active stream')\n      return\n    }\n\n    console.log('[ItoStreamController] Queueing config update')\n    const config = this.buildStreamConfig(context)\n    this.configQueue.push(config)\n  }\n\n  private sendModeUpdate(mode: ItoMode) {\n    console.log(`[ItoStreamController] Sending mode update: ${mode}`)\n\n    // Create a minimal config with just the mode\n    // IMPORTANT: Only set the mode field, leave others undefined so server merge works correctly\n    const contextInfo = create(ContextInfoSchema, {})\n    contextInfo.mode = mode\n    // Don't set windowTitle, appName, or contextText - let server keep existing values\n\n    const modeUpdate = create(TranscribeStreamRequestSchema, {\n      payload: {\n        case: 'config',\n        value: create(StreamConfigSchema, {\n          context: contextInfo,\n        }),\n      },\n    })\n\n    this.configQueue.push(modeUpdate)\n  }\n\n  public endInteraction() {\n    if (!this.audioStreamManager.isCurrentlyStreaming()) {\n      log.warn('[ItoStreamController] No active stream to end')\n      return\n    }\n\n    console.log('[ItoStreamController] Ending interaction stream')\n    this.stopStreaming()\n  }\n\n  public cancelTranscription() {\n    if (!this.audioStreamManager.isCurrentlyStreaming()) {\n      log.warn('[ItoStreamController] No active stream to cancel')\n      return\n    }\n\n    console.log('[ItoStreamController] Cancelling transcription')\n    this.isCancelled = true\n    this.abortController?.abort()\n\n    this.stopStreaming()\n  }\n\n  public getAudioDurationMs(): number {\n    return this.audioStreamManager.getAudioDurationMs()\n  }\n\n  private stopStreaming() {\n    this.audioStreamManager.stopStreaming()\n  }\n\n  public clearInteractionAudio() {\n    this.audioStreamManager.clearInteractionAudio()\n  }\n\n  private async *createStreamGenerator(): AsyncGenerator<TranscribeStreamRequest> {\n    console.log(\n      '[ItoStreamController] Starting stream generator (audio-first mode)',\n    )\n\n    // Stream audio chunks and interleave config updates\n    for await (const audioChunk of this.audioStreamManager.streamAudioChunks()) {\n      if (this.isCancelled) {\n        console.log(\n          '[ItoStreamController] Stream cancelled, stopping generator',\n        )\n        break\n      }\n\n      // Send any pending config updates before this audio chunk\n      while (this.configQueue.length > 0) {\n        const configMessage = this.configQueue.shift()!\n        console.log('[ItoStreamController] Sending config update from queue')\n        yield configMessage\n      }\n\n      // Send audio chunk\n      yield create(TranscribeStreamRequestSchema, {\n        payload: {\n          case: 'audioData',\n          value: audioChunk.audioData,\n        },\n      })\n    }\n\n    // Send any remaining config messages at the end\n    while (this.configQueue.length > 0) {\n      const configMessage = this.configQueue.shift()!\n      console.log(\n        '[ItoStreamController] Sending final config update from queue',\n      )\n      yield configMessage\n    }\n  }\n\n  private buildStreamConfig(context: ContextData): TranscribeStreamRequest {\n    const interactionId = interactionManager.getCurrentInteractionId()\n    // Build gRPC config message from the provided context data\n    return create(TranscribeStreamRequestSchema, {\n      payload: {\n        case: 'config',\n        value: create(StreamConfigSchema, {\n          context: create(ContextInfoSchema, {\n            windowTitle: context.windowTitle,\n            appName: context.appName,\n            contextText: context.contextText,\n            mode: this.currentMode,\n          }),\n          llmSettings: create(LlmSettingsSchema, {\n            asrModel: context.advancedSettings.llm.asrModel ?? undefined,\n            asrProvider: context.advancedSettings.llm.asrProvider ?? undefined,\n            asrPrompt: context.advancedSettings.llm.asrPrompt ?? undefined,\n            noSpeechThreshold:\n              context.advancedSettings.llm.noSpeechThreshold ?? undefined,\n            llmProvider: context.advancedSettings.llm.llmProvider ?? undefined,\n            llmModel: context.advancedSettings.llm.llmModel ?? undefined,\n            llmTemperature:\n              context.advancedSettings.llm.llmTemperature ?? undefined,\n\n            transcriptionPrompt:\n              context.advancedSettings.llm.transcriptionPrompt ?? undefined,\n            editingPrompt:\n              context.advancedSettings.llm.editingPrompt ?? undefined,\n          }),\n          vocabulary: context.vocabularyWords,\n          interactionId: interactionId || undefined,\n        }),\n      },\n    })\n  }\n}\n\nexport const itoStreamController = new ItoStreamController()\n"
  },
  {
    "path": "lib/main/logger.ts",
    "content": "import log from 'electron-log'\nimport { app } from 'electron'\nimport os from 'os'\nimport store, { getCurrentUserId } from './store'\nimport { STORE_KEYS } from '../constants/store-keys'\nimport { interactionManager } from './interactions/InteractionManager'\n\nconst LOG_QUEUE_KEY = 'log_queue:events'\n\nexport function initializeLogging() {\n  // Overriding console methods with electron-log\n  Object.assign(console, log.functions)\n\n  // Configure file transport for the packaged app\n  if (app.isPackaged) {\n    log.transports.file.level = 'info' // Log 'info' and higher (info, warn, error)\n    log.transports.file.format =\n      '[{y}-{m}-{d} {h}:{i}:{s}.{l}] [{processType}] [{level}] {text}'\n  } else {\n    log.transports.console.level = 'debug'\n    log.transports.file.level = false\n  }\n\n  // Set up IPC transport to receive logs from the renderer process\n  log.initialize()\n\n  log.info('Logging initialized.')\n  if (app.isPackaged) {\n    log.info(`Log file is located at: ${log.transports.file.getFile().path}`)\n  }\n\n  // Add remote transport to batch-forward client logs to server\n  type LogEvent = {\n    ts: number\n    level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'log'\n    message: string\n    fields?: Record<string, unknown>\n    interactionId: string | null\n    traceId?: string\n    spanId?: string\n    appVersion?: string\n    platform?: string\n    source?: string\n    loggedAtIso: string\n  }\n\n  const MAX_QUEUE = 5000\n  const initialEvents =\n    (store.get(LOG_QUEUE_KEY) as LogEvent[] | undefined) ?? []\n  const queue: LogEvent[] = [...initialEvents]\n\n  const persistQueue = () => {\n    store.set(LOG_QUEUE_KEY, queue)\n  }\n  let isSending = false\n  let flushTimer: ReturnType<typeof setTimeout> | null = null\n\n  const flush = async () => {\n    if (isSending || queue.length === 0) return\n    isSending = true\n    const take = Math.min(50, queue.length)\n    const batch = queue.slice(0, take)\n    try {\n      const baseUrl = import.meta.env.VITE_GRPC_BASE_URL\n      if (!baseUrl) return\n\n      const url = new URL('/logs', baseUrl)\n      const body = {\n        events: batch,\n      }\n      const token = (store.get(STORE_KEYS.ACCESS_TOKEN) as string | null) || ''\n      const res = await fetch(url.toString(), {\n        method: 'POST',\n        headers: {\n          'content-type': 'application/json',\n          ...(token ? { Authorization: `Bearer ${token}` } : {}),\n        },\n        body: JSON.stringify(body),\n      })\n      if (res.ok || res.status === 204) {\n        // Remove sent items only on success\n        queue.splice(0, take)\n        persistQueue()\n      }\n    } catch {\n      // Keep items in queue on failure; they remain persisted\n    } finally {\n      isSending = false\n    }\n  }\n\n  const scheduleFlush = () => {\n    if (flushTimer) return\n    flushTimer = setTimeout(async () => {\n      flushTimer = null\n      await flush()\n      if (queue.length > 0) {\n        // If more remain, schedule another cycle\n        scheduleFlush()\n      }\n    }, 2000)\n  }\n\n  const toEvent = (\n    level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'log',\n    message: string,\n    fields?: Record<string, unknown>,\n  ) => {\n    const userId = getCurrentUserId()\n    const interactionId = interactionManager.getCurrentInteractionId()\n    const now = Date.now()\n    const event: LogEvent = {\n      ts: now,\n      loggedAtIso: new Date(now).toISOString(),\n      level,\n      message,\n      fields: {\n        ...fields,\n        userId,\n        hostname: os.hostname(),\n        platform: process.platform,\n        arch: process.arch,\n      },\n      interactionId,\n      appVersion: app.getVersion?.() ?? 'unknown',\n      platform: `${process.platform}-${process.arch}`,\n      source: 'client',\n    }\n    return event\n  }\n\n  // Wrap core log methods to enqueue events\n  const originalInfo = console.info\n  const originalWarn = console.warn\n  const originalError = console.error\n  const originalLog = console.log\n\n  console.log = (...args: any[]) => {\n    try {\n      queue.push(toEvent('log', String(args[0] ?? ''), { args }))\n      if (queue.length > MAX_QUEUE) queue.splice(0, queue.length - MAX_QUEUE)\n      persistQueue()\n      scheduleFlush()\n    } catch (err) {\n      originalError('Failed to enqueue log event (log):', err)\n    }\n    originalLog.apply(console, args as any)\n  }\n  console.info = (...args: any[]) => {\n    try {\n      queue.push(toEvent('info', String(args[0] ?? ''), { args }))\n      if (queue.length > MAX_QUEUE) queue.splice(0, queue.length - MAX_QUEUE)\n      persistQueue()\n      scheduleFlush()\n    } catch (err) {\n      originalError('Failed to enqueue log event (info):', err)\n    }\n    originalInfo.apply(console, args as any)\n  }\n  console.warn = (...args: any[]) => {\n    try {\n      queue.push(toEvent('warn', String(args[0] ?? ''), { args }))\n      if (queue.length > MAX_QUEUE) queue.splice(0, queue.length - MAX_QUEUE)\n      persistQueue()\n      scheduleFlush()\n    } catch (err) {\n      originalError('Failed to enqueue log event (warn):', err)\n    }\n    originalWarn.apply(console, args as any)\n  }\n  console.error = (...args: any[]) => {\n    try {\n      queue.push(toEvent('error', String(args[0] ?? ''), { args }))\n      if (queue.length > MAX_QUEUE) queue.splice(0, queue.length - MAX_QUEUE)\n      persistQueue()\n      scheduleFlush()\n    } catch (err) {\n      originalError('Failed to enqueue log event (error):', err)\n    }\n    originalError.apply(console, args as any)\n  }\n\n  // Also wrap electron-log methods (log.info, log.warn, etc.) so direct calls are sent\n  const levelMap: Record<string, 'debug' | 'info' | 'warn' | 'error'> = {\n    verbose: 'debug',\n    silly: 'debug',\n    debug: 'debug',\n    info: 'info',\n    log: 'info',\n    warn: 'warn',\n    error: 'error',\n  }\n  ;(\n    ['info', 'warn', 'error', 'debug', 'verbose', 'silly', 'log'] as const\n  ).forEach(method => {\n    const original = (log as any)[method]?.bind(log)\n    if (typeof original !== 'function') return\n    ;(log as any)[method] = (...args: any[]) => {\n      try {\n        const mapped = levelMap[method] || 'info'\n        queue.push(toEvent(mapped as any, String(args[0] ?? ''), { args }))\n        if (queue.length > MAX_QUEUE) queue.splice(0, queue.length - MAX_QUEUE)\n        persistQueue()\n        scheduleFlush()\n      } catch (err) {\n        originalError(`Failed to enqueue electron-log event (${method}):`, err)\n      }\n      return original(...args)\n    }\n  })\n\n  // Best-effort flush when the app is quitting; durability is ensured by persistence\n  app.on('before-quit', () => {\n    void flush()\n  })\n\n  // If there are persisted events on startup, schedule an initial flush\n  if (queue.length > 0) {\n    scheduleFlush()\n  }\n}\n"
  },
  {
    "path": "lib/main/main.ts",
    "content": "import './env'\nimport './sentry'\nimport { app, protocol } from 'electron'\nimport { electronApp, optimizer } from '@electron-toolkit/utils'\nimport {\n  createAppWindow,\n  createPillWindow,\n  mainWindow,\n  registerResourcesProtocol,\n  startPillPositioner,\n} from './app'\nimport { initializeLogging } from './logger'\nimport { registerIPC } from '../window/ipcEvents'\nimport { registerDevIPC } from '../window/ipcDev'\nimport { initializeDatabase } from './sqlite/db'\nimport { setupProtocolHandling, processStartupProtocolUrl } from '../protocol'\nimport { startKeyListener } from '../media/keyboard'\n// Import the grpcClient singleton\nimport { grpcClient } from '../clients/grpcClient'\nimport { preventAppNap } from './appNap'\nimport { syncService } from './syncService'\nimport { checkAccessibilityPermission } from '../utils/crossPlatform'\nimport mainStore, { initializeStore } from './store'\nimport { STORE_KEYS } from '../constants/store-keys'\nimport { selectedTextReaderService } from '../media/selected-text-reader'\nimport { macOSAccessibilityContextProvider } from '../media/macOSAccessibilityContextProvider'\nimport { voiceInputService } from './voiceInputService'\nimport { initializeMicrophoneSelection } from '../media/microphoneSetUp'\nimport { validateStoredTokens, ensureValidTokens } from '../auth/events'\nimport { Auth0Config, validateAuth0Config } from '../auth/config'\nimport { createAppTray } from './tray'\nimport { itoSessionManager } from './itoSessionManager'\nimport { initializeAutoUpdater } from './autoUpdaterWrapper'\nimport { teardown } from './teardown'\nimport { ITO_ENV } from './env'\n\nprotocol.registerSchemesAsPrivileged([\n  {\n    scheme: 'res',\n    privileges: { standard: true, secure: true, supportFetchAPI: true },\n  },\n])\n\n// This method will be called when Electron has finished\n// initialization and is ready to create browser windows.\n// Some APIs can only be used after this event occurs.\napp.whenReady().then(async () => {\n  // Initialize the database BEFORE logging so KV writes have a schema\n  try {\n    await initializeDatabase()\n  } catch (error) {\n    console.error('Failed to initialize database, quitting app.', error)\n    return\n  }\n\n  // Initialize KV-backed store and run migrations before anything reads/writes\n  try {\n    await initializeStore()\n  } catch (err) {\n    console.error('Failed to initialize main store, quitting app.', err)\n    return\n  }\n\n  // Initialize logging after DB + store so batched log persistence can write\n  initializeLogging()\n\n  // Validate Auth0 configuration\n  try {\n    validateAuth0Config()\n  } catch (error) {\n    console.error('Auth0 configuration error:', error)\n    console.warn(\n      'Token refresh will be disabled due to missing Auth0 configuration',\n    )\n  }\n\n  // Validate stored tokens before using them (will attempt refresh if needed)\n  const tokensAreValid = await validateStoredTokens(Auth0Config)\n\n  // If we have valid tokens from a previous session, start the sync service\n  if (tokensAreValid) {\n    const accessToken = mainStore.get(STORE_KEYS.ACCESS_TOKEN) as\n      | string\n      | undefined\n    if (accessToken) {\n      grpcClient.setAuthToken(accessToken)\n      syncService.start()\n    }\n  }\n\n  // Setup protocol handling for deep links\n  setupProtocolHandling()\n\n  // Prevent app nap\n  preventAppNap()\n\n  // Register the handler for the 'res' protocol now that the app is ready.\n  const appId = ITO_ENV === 'prod' ? 'ai.ito.ito' : `ai.ito.ito-${ITO_ENV}`\n  registerResourcesProtocol()\n  electronApp.setAppUserModelId(appId)\n\n  // IMPORTANT: Register IPC handlers BEFORE creating windows\n  // This prevents the renderer from making IPC calls before handlers are ready\n  registerIPC()\n\n  if (!app.isPackaged) {\n    registerDevIPC()\n  }\n\n  // Create windows\n  createAppWindow()\n  createPillWindow()\n  startPillPositioner()\n\n  // Handle protocol URL if the app was started by a deep link (Windows first instance)\n  processStartupProtocolUrl()\n\n  // --- ADDED: Give the gRPC client a reference to the main window ---\n  // This allows it to send transcription results back to the renderer.\n  if (mainWindow) {\n    grpcClient.setMainWindow(mainWindow)\n  }\n\n  if (checkAccessibilityPermission(false)) {\n    console.log('Accessibility permissions found, starting key listener.')\n    startKeyListener()\n  }\n\n  console.log('Microphone access granted, starting audio recorder.')\n  voiceInputService.setUpAudioRecorderListeners()\n\n  console.log('Starting selected text reader service.')\n  selectedTextReaderService.initialize()\n\n  // Initialize cursor context provider (macOS only for now)\n  if (process.platform === 'darwin') {\n    console.log('Starting cursor context provider.')\n    macOSAccessibilityContextProvider.initialize()\n  }\n\n  // Initialize microphone selection to prefer built-in microphone\n  await initializeMicrophoneSelection()\n\n  // Create system tray after audio recorder is initialized and devices are available\n  await createAppTray()\n\n  app.on('activate', function () {\n    if (mainWindow === null) {\n      createAppWindow()\n      // Update the gRPC client with the new main window reference\n      if (mainWindow) {\n        grpcClient.setMainWindow(mainWindow)\n      }\n    }\n  })\n\n  app.on('before-quit', () => {\n    console.log('App is quitting, cleaning up resources...')\n    teardown()\n  })\n\n  app.on('browser-window-created', (_, window) => {\n    optimizer.watchWindowShortcuts(window)\n  })\n\n  // Initialize auto-updater\n  initializeAutoUpdater()\n\n  // Set up periodic token refresh check (every 10 minutes)\n  setInterval(\n    async () => {\n      try {\n        await ensureValidTokens(Auth0Config)\n      } catch (error) {\n        console.error('Periodic token refresh failed:', error)\n      }\n    },\n    10 * 60 * 1000,\n  ) // Check every 10 minutes\n})\n\napp.on('window-all-closed', () => {\n  // We want the app to stay alive\n})\n"
  },
  {
    "path": "lib/main/recordingStateNotifier.ts",
    "content": "import { ItoMode } from '@/app/generated/ito_pb'\nimport { getPillWindow, mainWindow } from './app'\nimport {\n  IPC_EVENTS,\n  RecordingStatePayload,\n  ProcessingStatePayload,\n} from '../types/ipc'\n\n/**\n * Helper class to notify UI windows about recording state changes.\n */\nexport class RecordingStateNotifier {\n  public notifyRecordingStarted(mode: ItoMode) {\n    console.log('[RecordingStateNotifier] Notifying recording started:', {\n      mode,\n    })\n    this.sendToWindows(IPC_EVENTS.RECORDING_STATE_UPDATE, {\n      isRecording: true,\n      mode,\n    })\n  }\n\n  public notifyRecordingStopped() {\n    console.log('[RecordingStateNotifier] Notifying recording stopped')\n    this.sendToWindows(IPC_EVENTS.RECORDING_STATE_UPDATE, {\n      isRecording: false,\n    })\n  }\n\n  public notifyProcessingStarted() {\n    console.log('[RecordingStateNotifier] Notifying processing started')\n    this.sendToWindows(IPC_EVENTS.PROCESSING_STATE_UPDATE, {\n      isProcessing: true,\n    })\n  }\n\n  public notifyProcessingStopped() {\n    console.log('[RecordingStateNotifier] Notifying processing stopped')\n    this.sendToWindows(IPC_EVENTS.PROCESSING_STATE_UPDATE, {\n      isProcessing: false,\n    })\n  }\n\n  private sendToWindows(\n    event: string,\n    payload: RecordingStatePayload | ProcessingStatePayload,\n  ) {\n    // Send to pill window\n    getPillWindow()?.webContents.send(event, payload)\n\n    // Send to main window if it exists and is not destroyed\n    if (\n      mainWindow &&\n      !mainWindow.isDestroyed() &&\n      !mainWindow.webContents.isDestroyed()\n    ) {\n      mainWindow.webContents.send(event, payload)\n    }\n  }\n}\n\nexport const recordingStateNotifier = new RecordingStateNotifier()\n"
  },
  {
    "path": "lib/main/sentry.ts",
    "content": "import * as Sentry from '@sentry/electron/main'\n\nconst dsn = (import.meta as any).env?.VITE_SENTRY_DSN as string | undefined\nconst environment =\n  ((import.meta as any).env?.VITE_SENTRY_ENV as string | undefined) || 'local'\n\nconst tracesSampleRateRaw = (import.meta as any).env\n  ?.VITE_SENTRY_TRACES_SAMPLE_RATE\nconst tracesSampleRate = Number.parseFloat(\n  typeof tracesSampleRateRaw === 'string' && tracesSampleRateRaw.trim() !== ''\n    ? tracesSampleRateRaw\n    : '0.2',\n)\n\nconst profilesSampleRateRaw = (import.meta as any).env\n  ?.VITE_SENTRY_PROFILES_SAMPLE_RATE\nconst profilesSampleRate = Number.parseFloat(\n  typeof profilesSampleRateRaw === 'string' &&\n    profilesSampleRateRaw.trim() !== ''\n    ? profilesSampleRateRaw\n    : '0.2',\n)\n\nSentry.init({\n  enabled: Boolean(dsn),\n  dsn,\n  environment,\n  tracesSampleRate,\n  profilesSampleRate,\n})\n"
  },
  {
    "path": "lib/main/sqlite/db.test.ts",
    "content": "import { describe, test, expect, beforeEach, mock } from 'bun:test'\n\n// Mock electron BEFORE importing db.ts\nmock.module('electron', () => ({\n  app: {\n    getPath: mock((type: string) => {\n      if (type === 'userData') return '/tmp/test-ito-app'\n      return '/tmp/test-path'\n    }),\n    quit: mock(),\n  },\n}))\n\n// Mock other dependencies\nmock.module('../../clients/grpcClient', () => ({\n  grpcClient: {\n    deleteUserData: mock(),\n  },\n}))\n\nmock.module('electron-store', () => {\n  const MockStore = function (this: any) {\n    this.data = new Map()\n  }\n  MockStore.prototype.get = mock(function (this: any, key: any) {\n    return this.data.get(key)\n  })\n  MockStore.prototype.set = mock(function (this: any, key: any, value: any) {\n    this.data.set(key, value)\n  })\n  return { default: MockStore }\n})\n\nmock.module('../store', () => ({\n  default: { get: mock(), set: mock() },\n  getCurrentUserId: mock(() => 'test-user-123'),\n}))\n\n// Mock file system\nconst mockFs = {\n  unlink: mock(),\n  mkdir: mock(() => Promise.resolve()),\n}\nmock.module('fs', () => ({\n  promises: mockFs,\n}))\n\n// Mock database utilities - minimal mocking for business logic tests\nconst mockRun = mock()\nconst mockExec = mock()\nconst mockGet = mock()\nconst mockAll = mock()\n\nmock.module('./utils', () => ({\n  run: mockRun,\n  exec: mockExec,\n  get: mockGet,\n  all: mockAll,\n}))\n\n// Mock sqlite3 with basic functionality\nconst mockSqliteDatabase: any = {\n  close: mock((callback?: (err: Error | null) => void) => {\n    if (callback) {\n      setTimeout(() => {\n        callback(null)\n      }, 10)\n    }\n  }),\n  run: mock(),\n  exec: mock(),\n}\n\nmock.module('sqlite3', () => ({\n  default: {\n    Database: mock((_path: string, callback?: (err: Error | null) => void) => {\n      setTimeout(() => callback?.(null), 0)\n      return mockSqliteDatabase\n    }),\n  },\n}))\n\n// Test data for migrations\nconst MOCK_MIGRATIONS = [\n  {\n    id: '20250108120000_add_raw_audio_to_interactions',\n    up: 'ALTER TABLE interactions ADD COLUMN raw_audio BLOB;',\n    down: 'ALTER TABLE interactions DROP COLUMN raw_audio;',\n  },\n  {\n    id: '20250108130000_add_duration_to_interactions',\n    up: 'ALTER TABLE interactions ADD COLUMN duration_ms INTEGER DEFAULT 0;',\n    down: 'ALTER TABLE interactions DROP COLUMN duration_ms;',\n  },\n]\n\nmock.module('./migrations', () => ({\n  MIGRATIONS: MOCK_MIGRATIONS,\n}))\n\nmock.module('./schema', () => ({\n  INITIAL_SCHEMA: `CREATE TABLE interactions (id TEXT PRIMARY KEY);`,\n}))\n\nimport {\n  initializeDatabase,\n  getDb,\n  revertLastMigration,\n  wipeDatabase,\n} from './db'\nimport path from 'path'\n\ndescribe('Database State Management', () => {\n  beforeEach(() => {\n    // Clear all mocks\n    mockRun.mockClear()\n    mockExec.mockClear()\n    mockGet.mockClear()\n    mockAll.mockClear()\n    mockFs.unlink.mockClear()\n\n    // Reset module state by clearing require cache\n    delete require.cache[require.resolve('./db')]\n  })\n\n  test('should throw error when accessing uninitialized database', async () => {\n    // Import fresh module to ensure uninitialized state\n    const { getDb: freshGetDb } = await import('./db')\n\n    expect(() => freshGetDb()).toThrow(\n      'Database not initialized. Call initializeDatabase() first.',\n    )\n  })\n\n  test('should allow database access after initialization', async () => {\n    mockAll.mockResolvedValue([]) // No existing migrations\n    mockRun.mockResolvedValue(undefined)\n    mockExec.mockResolvedValue(undefined)\n\n    await initializeDatabase()\n    const db = getDb()\n\n    expect(db).toBeDefined()\n    expect(db).toBe(mockSqliteDatabase)\n  })\n\n  test('should handle database connection errors', async () => {\n    // Mock database connection failure\n    const failingDatabase = mock(\n      (_path: string, callback?: (err: Error | null) => void) => {\n        setTimeout(() => callback?.(new Error('Connection failed')), 0)\n        return mockSqliteDatabase\n      },\n    )\n\n    // Temporarily replace the Database constructor\n    const sqlite3Module = await import('sqlite3')\n    const originalDatabase = sqlite3Module.default.Database\n    sqlite3Module.default.Database = failingDatabase as any\n\n    try {\n      expect(initializeDatabase()).rejects.toThrow('Connection failed')\n    } finally {\n      sqlite3Module.default.Database = originalDatabase\n    }\n  })\n})\n\ndescribe('Migration Logic', () => {\n  beforeEach(() => {\n    mockRun.mockClear()\n    mockExec.mockClear()\n    mockGet.mockClear()\n    mockAll.mockClear()\n  })\n\n  test('should identify which migrations need to be applied', async () => {\n    // Mock that first migration is already applied\n    mockAll.mockResolvedValue([\n      { id: '0000_initial_schema' },\n      { id: '20250108120000_add_raw_audio_to_interactions' },\n    ])\n    mockRun.mockResolvedValue(undefined)\n    mockExec.mockResolvedValue(undefined)\n\n    await initializeDatabase()\n\n    // Should only run the remaining migration\n    expect(mockExec).toHaveBeenCalledWith(\n      'ALTER TABLE interactions ADD COLUMN duration_ms INTEGER DEFAULT 0;',\n    )\n\n    // Should record the new migration\n    expect(mockRun).toHaveBeenCalledWith(\n      'INSERT INTO migrations (id, applied_at) VALUES (?, ?)',\n      ['20250108130000_add_duration_to_interactions', expect.any(String)],\n    )\n  })\n\n  test('should skip all migrations when database is up to date', async () => {\n    // Mock all migrations already applied\n    mockAll.mockResolvedValue([\n      { id: '0000_initial_schema' },\n      { id: '20250108120000_add_raw_audio_to_interactions' },\n      { id: '20250108130000_add_duration_to_interactions' },\n    ])\n\n    const consoleSpy = mock()\n    const originalInfo = console.info\n    console.info = consoleSpy\n\n    try {\n      await initializeDatabase()\n      expect(consoleSpy).toHaveBeenCalledWith('Database schema is up to date.')\n    } finally {\n      console.info = originalInfo\n    }\n  })\n\n  test('should handle migration failure with proper rollback', async () => {\n    mockAll.mockResolvedValue([]) // No existing migrations\n\n    // Setup exec to fail on schema execution\n    let callCount = 0\n    mockExec.mockImplementation(() => {\n      callCount++\n      if (callCount === 1) return Promise.resolve(undefined) // BEGIN\n      if (callCount === 2) return Promise.reject(new Error('Migration failed')) // Schema fails\n      if (callCount === 3) return Promise.resolve(undefined) // ROLLBACK\n      return Promise.resolve(undefined)\n    })\n\n    await expect(initializeDatabase()).rejects.toThrow(\n      'Migration 0000_initial_schema failed.',\n    )\n\n    expect(mockExec).toHaveBeenCalledWith('ROLLBACK;')\n  })\n})\n\ndescribe('Migration Validation', () => {\n  beforeEach(() => {\n    mockGet.mockClear()\n    mockExec.mockClear()\n    mockRun.mockClear()\n  })\n\n  test('should prevent reverting initial schema', async () => {\n    mockGet.mockResolvedValue({ id: '0000_initial_schema' })\n\n    expect(revertLastMigration()).rejects.toThrow(\n      'Reverting the initial schema is not supported.',\n    )\n  })\n\n  test('should handle migration not found in code', async () => {\n    mockGet.mockResolvedValue({ id: 'unknown_migration_12345' })\n\n    expect(revertLastMigration()).rejects.toThrow(\n      'Migration with id unknown_migration_12345 found in DB but not in code.',\n    )\n  })\n\n  test('should handle no migrations to revert', async () => {\n    mockGet.mockResolvedValue(null)\n\n    const consoleSpy = mock()\n    const originalInfo = console.info\n    console.info = consoleSpy\n\n    try {\n      await revertLastMigration()\n      expect(consoleSpy).toHaveBeenCalledWith('No migrations to revert.')\n    } finally {\n      console.info = originalInfo\n    }\n  })\n\n  test('should successfully revert valid migration', async () => {\n    // Mock finding a valid migration to revert\n    mockGet.mockResolvedValue({\n      id: '20250108130000_add_duration_to_interactions',\n    })\n    mockExec.mockResolvedValue(undefined)\n    mockRun.mockResolvedValue(undefined)\n\n    await revertLastMigration()\n\n    // Should execute the down script\n    expect(mockExec).toHaveBeenCalledWith(\n      'ALTER TABLE interactions DROP COLUMN duration_ms;',\n    )\n\n    // Should remove migration from database\n    expect(mockRun).toHaveBeenCalledWith(\n      'DELETE FROM migrations WHERE id = ?',\n      ['20250108130000_add_duration_to_interactions'],\n    )\n  })\n\n  test('should rollback on revert failure', async () => {\n    mockGet.mockResolvedValue({\n      id: '20250108130000_add_duration_to_interactions',\n    })\n    mockExec.mockResolvedValueOnce(undefined) // BEGIN\n    mockExec.mockRejectedValueOnce(new Error('Revert failed')) // DOWN script fails\n    mockExec.mockResolvedValueOnce(undefined) // ROLLBACK\n\n    expect(revertLastMigration()).rejects.toThrow(\n      'Migration 20250108130000_add_duration_to_interactions revert failed.',\n    )\n\n    expect(mockExec).toHaveBeenCalledWith('ROLLBACK;')\n  })\n})\n\ndescribe('File Error Handling', () => {\n  beforeEach(() => {\n    // Clear all mocks\n    mockRun.mockClear()\n    mockExec.mockClear()\n    mockGet.mockClear()\n    mockAll.mockClear()\n    mockFs.unlink.mockClear()\n    mockSqliteDatabase.close.mockClear()\n    mockSqliteDatabase.run.mockClear()\n    mockSqliteDatabase.exec.mockClear()\n\n    // Reset module state by clearing require cache\n    delete require.cache[require.resolve('./db')]\n  })\n\n  /* Bun sucks and doesn't support mocked errors in tests */\n  // test('should handle file not found gracefully during wipe', async () => {\n  //   const fileNotFoundError = new Error('File not found')\n  //   ;(fileNotFoundError as any).code = 'ENOENT'\n  //   mockFs.unlink.mockRejectedValue(fileNotFoundError)\n\n  //   const consoleSpy = mock()\n  //   const originalInfo = console.info\n  //   console.info = consoleSpy\n\n  //   try {\n  //     await wipeDatabase()\n  //     expect(consoleSpy).toHaveBeenCalledWith(\n  //       'Database file did not exist, skipping deletion.',\n  //     )\n  //   } finally {\n  //     console.info = originalInfo\n  //   }\n  // })\n\n  // test('should rethrow non-ENOENT file errors', async () => {\n  //   const permissionError = new Error('Permission denied')\n  //   ;(permissionError as any).code = 'EPERM'\n  //   mockFs.unlink.mockRejectedValue(permissionError)\n\n  //   expect(wipeDatabase()).rejects.toThrow('Permission denied')\n  // })\n\n  test('should successfully delete database file when it exists', async () => {\n    mockFs.unlink.mockResolvedValue(undefined)\n\n    await wipeDatabase()\n\n    const expectedPath = path.join('/tmp/test-ito-app', 'ito.db')\n    expect(mockFs.unlink).toHaveBeenCalledWith(expectedPath)\n  })\n})\n\ndescribe('Timestamp Generation', () => {\n  test('should generate valid ISO timestamps for migrations', async () => {\n    mockAll.mockResolvedValue([])\n    mockRun.mockResolvedValue(undefined)\n    mockExec.mockResolvedValue(undefined)\n\n    const beforeTime = Date.now()\n    await initializeDatabase()\n    const afterTime = Date.now()\n\n    // Verify migration timestamp is valid and within reasonable range\n    const migrationCall = mockRun.mock.calls.find(call =>\n      call[0].includes('INSERT INTO migrations'),\n    )\n    expect(migrationCall).toBeDefined()\n\n    if (!migrationCall) {\n      // Fail the test\n      expect(1).toBe(2)\n      return\n    }\n\n    const timestamp = migrationCall[1][1] // second parameter is timestamp\n    const timestampTime = new Date(timestamp).getTime()\n\n    expect(() => new Date(timestamp)).not.toThrow()\n    expect(timestampTime).toBeGreaterThanOrEqual(beforeTime)\n    expect(timestampTime).toBeLessThanOrEqual(afterTime)\n  })\n})\n"
  },
  {
    "path": "lib/main/sqlite/db.ts",
    "content": "import sqlite3 from 'sqlite3'\nimport { app } from 'electron'\nimport path from 'path'\nimport { promises as fs } from 'fs'\nimport { INITIAL_SCHEMA } from './schema'\nimport { MIGRATIONS, Migration } from './migrations'\nimport { run, exec, get, all } from './utils'\n\nconst DB_FILE = 'ito.db'\nconst dbPath = path.join(app.getPath('userData'), DB_FILE)\n\nlet db: sqlite3.Database\n\nconst runMigrations = async () => {\n  // 1. Create migrations table if it doesn't exist\n  await run(\n    'CREATE TABLE IF NOT EXISTS migrations (id TEXT PRIMARY KEY, applied_at TEXT NOT NULL)',\n  )\n\n  // 2. Get applied migrations\n  const appliedMigrations = await all<{ id: string }>(\n    'SELECT id FROM migrations',\n  )\n  const appliedMigrationIds = new Set(appliedMigrations.map(row => row.id))\n\n  // 3. Define all migrations, including initial schema as the first one\n  const allMigrations: Migration[] = [\n    {\n      id: '0000_initial_schema',\n      up: INITIAL_SCHEMA,\n      down: `\n        DROP TABLE IF EXISTS notes;\n        DROP TABLE IF EXISTS dictionary_items;\n        DROP TABLE IF EXISTS interactions;\n        DROP TABLE IF EXISTS key_value_store;\n      `,\n    },\n    ...MIGRATIONS,\n  ]\n\n  // 4. Filter out migrations that have already been applied\n  const pendingMigrations = allMigrations.filter(\n    m => !appliedMigrationIds.has(m.id),\n  )\n\n  if (pendingMigrations.length === 0) {\n    console.info('Database schema is up to date.')\n    return\n  }\n\n  // 5. Apply pending migrations\n  console.info(\n    `Found ${pendingMigrations.length} pending migrations. Applying...`,\n  )\n\n  for (const migration of pendingMigrations) {\n    console.info(`Applying migration: ${migration.id}`)\n    try {\n      // We use exec for migrations that might contain multiple statements\n      // like the initial schema.\n      await exec('BEGIN;')\n      await exec(migration.up)\n      await run('INSERT INTO migrations (id, applied_at) VALUES (?, ?)', [\n        migration.id,\n        new Date().toISOString(),\n      ])\n      await exec('COMMIT;')\n      console.info(`Migration ${migration.id} applied successfully.`)\n    } catch (err) {\n      console.error(`Failed to apply migration ${migration.id}:`, err)\n      // Rollback transaction on error\n      await exec('ROLLBACK;')\n      throw new Error(`Migration ${migration.id} failed.`)\n    }\n  }\n}\n\nconst revertLastMigration = async () => {\n  // 1. Get the last applied migration\n  const lastMigration = await get<{ id: string }>(\n    'SELECT id FROM migrations ORDER BY applied_at DESC LIMIT 1',\n  )\n\n  if (!lastMigration) {\n    console.info('No migrations to revert.')\n    return\n  }\n\n  // 2. Define all migrations to find the one to revert\n  const allMigrations: Migration[] = [\n    {\n      id: '0000_initial_schema',\n      up: INITIAL_SCHEMA,\n      down: `\n        DROP TABLE IF EXISTS notes;\n        DROP TABLE IF EXISTS dictionary_items;\n        DROP TABLE IF EXISTS interactions;\n        DROP TABLE IF EXISTS key_value_store;\n      `,\n    },\n    ...MIGRATIONS,\n  ]\n  const migrationToRevert = allMigrations.find(m => m.id === lastMigration.id)\n\n  if (!migrationToRevert) {\n    throw new Error(\n      `Migration with id ${lastMigration.id} found in DB but not in code.`,\n    )\n  }\n\n  // We are not allowing reverting the initial schema for safety.\n  if (migrationToRevert.id === '0000_initial_schema') {\n    console.error(\n      'Cannot revert initial schema. To reset the database, delete the database file.',\n    )\n    throw new Error('Reverting the initial schema is not supported.')\n  }\n\n  // 3. Apply the 'down' script in a transaction\n  console.info(`Reverting migration: ${migrationToRevert.id}`)\n  try {\n    await exec('BEGIN;')\n    await exec(migrationToRevert.down)\n    await run('DELETE FROM migrations WHERE id = ?', [migrationToRevert.id])\n    await exec('COMMIT;')\n    console.info(\n      `Migration ${migrationToRevert.id} reverted successfully. App will quit, please relaunch.`,\n    )\n    app.quit()\n  } catch (err) {\n    console.error(`Failed to revert migration ${migrationToRevert.id}:`, err)\n    await exec('ROLLBACK;')\n    throw new Error(`Migration ${migrationToRevert.id} revert failed.`)\n  }\n}\n\nconst wipeDatabase = async () => {\n  if (db) {\n    await new Promise<void>((resolve, reject) => {\n      db.close(err => {\n        if (err) return reject(err)\n        resolve()\n      })\n    })\n    console.info('Database connection closed.')\n  }\n\n  try {\n    await fs.unlink(dbPath)\n    console.info('Database file deleted.')\n  } catch (error: any) {\n    if (error.code !== 'ENOENT') {\n      throw error\n    }\n    console.info('Database file did not exist, skipping deletion.')\n  }\n\n  console.info('Database wiped. Application will now quit, please relaunch.')\n  app.quit()\n}\n\nconst deleteUserData = async (userId: string) => {\n  console.info(`Deleting all data for user: ${userId}`)\n\n  try {\n    // Import the repository classes\n    const { InteractionsTable, NotesTable, DictionaryTable } = await import(\n      './repo'\n    )\n\n    // Delete all user data from all tables\n    await Promise.all([\n      InteractionsTable.deleteAllUserData(userId),\n      NotesTable.deleteAllUserData(userId),\n      DictionaryTable.deleteAllUserData(userId),\n    ])\n\n    console.info(`Successfully deleted all data for user: ${userId}`)\n  } catch (error) {\n    console.error(`Failed to delete user data for user: ${userId}`, error)\n    throw error\n  }\n}\n\nconst deleteCompleteUserData = async (userId: string) => {\n  console.info(`Starting complete data deletion for user: ${userId}`)\n\n  try {\n    // Delete local SQLite data\n    await deleteUserData(userId)\n\n    // Delete server-side data - server will extract userId from authenticated token\n    const { grpcClient } = await import('../../clients/grpcClient')\n    await grpcClient.deleteUserData()\n\n    console.info(`Successfully completed data deletion for user: ${userId}`)\n  } catch (error) {\n    console.error(`Failed to complete data deletion for user: ${userId}`, error)\n    throw error\n  }\n}\n\nconst initializeDatabase = (): Promise<void> => {\n  return new Promise((resolve, reject) => {\n    db = new sqlite3.Database(dbPath, err => {\n      if (err) {\n        console.error('Failed to connect to SQLite database.', err)\n        reject(err)\n      } else {\n        console.info('Connected to SQLite database.')\n        runMigrations()\n          .then(resolve)\n          .catch(e => {\n            console.error('Failed to run migrations.', e)\n            reject(e)\n          })\n      }\n    })\n  })\n}\n\nconst getDb = () => {\n  if (!db) {\n    throw new Error(\n      'Database not initialized. Call initializeDatabase() first.',\n    )\n  }\n  return db\n}\n\nexport {\n  initializeDatabase,\n  getDb,\n  revertLastMigration,\n  wipeDatabase,\n  deleteUserData,\n  deleteCompleteUserData,\n}\n"
  },
  {
    "path": "lib/main/sqlite/migrations.ts",
    "content": "export interface Migration {\n  id: string\n  up: string\n  down: string\n}\n\nexport const MIGRATIONS: Migration[] = [\n  {\n    id: '20250108120000_add_raw_audio_to_interactions',\n    up: 'ALTER TABLE interactions ADD COLUMN raw_audio BLOB;',\n    down: 'ALTER TABLE interactions DROP COLUMN raw_audio;',\n  },\n  {\n    id: '20250108130000_add_duration_to_interactions',\n    up: 'ALTER TABLE interactions ADD COLUMN duration_ms INTEGER DEFAULT 0;',\n    down: 'ALTER TABLE interactions DROP COLUMN duration_ms;',\n  },\n  {\n    id: '20250110120000_add_sample_rate_to_interactions',\n    up: 'ALTER TABLE interactions ADD COLUMN sample_rate INTEGER;',\n    down: 'ALTER TABLE interactions DROP COLUMN sample_rate;',\n  },\n  {\n    id: '20250111120000_add_raw_audio_id_to_interactions',\n    up: 'ALTER TABLE interactions ADD COLUMN raw_audio_id TEXT;',\n    down: 'ALTER TABLE interactions DROP COLUMN raw_audio_id;',\n  },\n  {\n    id: '20250923091139_make_dictionary_word_unique',\n    up: `\n      -- Delete duplicate entries, keeping only the most recent one (highest id)\n      DELETE FROM dictionary_items\n      WHERE id NOT IN (\n        SELECT MAX(id)\n        FROM dictionary_items\n        WHERE deleted_at IS NULL\n        GROUP BY word\n      )\n      AND deleted_at IS NULL;\n\n      -- Now create the unique index\n      CREATE UNIQUE INDEX idx_dictionary_items_word_unique ON dictionary_items(word) WHERE deleted_at IS NULL;\n    `,\n    down: 'DROP INDEX idx_dictionary_items_word_unique;',\n  },\n  {\n    id: '20251029000000_add_user_metadata_table',\n    up: `\n      CREATE TABLE user_metadata (\n        id TEXT PRIMARY KEY,\n        user_id TEXT NOT NULL UNIQUE,\n        paid_status TEXT NOT NULL DEFAULT 'FREE',\n        free_words_remaining INTEGER,\n        pro_trial_start_date TEXT,\n        pro_trial_end_date TEXT,\n        pro_subscription_start_date TEXT,\n        pro_subscription_end_date TEXT,\n        created_at TEXT NOT NULL,\n        updated_at TEXT NOT NULL\n      );\n    `,\n    down: 'DROP TABLE user_metadata;',\n  },\n]\n"
  },
  {
    "path": "lib/main/sqlite/models.ts",
    "content": "export interface Interaction {\n  id: string\n  user_id: string | null\n  title: string | null\n  asr_output: any\n  llm_output: any\n  raw_audio: Buffer | null\n  raw_audio_id: string | null\n  duration_ms: number | null\n  sample_rate: number | null\n  created_at: string\n  updated_at: string\n  deleted_at: string | null\n}\n\nexport interface Note {\n  id: string\n  user_id: string\n  interaction_id: string | null\n  content: string\n  created_at: string\n  updated_at: string\n  deleted_at: string | null\n}\n\nexport interface DictionaryItem {\n  id: string\n  user_id: string\n  word: string\n  pronunciation: string | null\n  created_at: string\n  updated_at: string\n  deleted_at: string | null\n}\n\nexport enum PaidStatus {\n  FREE = 'FREE',\n  PRO_TRIAL = 'PRO_TRIAL',\n  PRO = 'PRO',\n}\n\nexport interface UserMetadata {\n  id: string\n  user_id: string\n  paid_status: PaidStatus\n  free_words_remaining: number | null\n  pro_trial_start_date: Date | null\n  pro_trial_end_date: Date | null\n  pro_subscription_start_date: Date | null\n  pro_subscription_end_date: Date | null\n  created_at: Date\n  updated_at: Date\n}\n"
  },
  {
    "path": "lib/main/sqlite/repo.test.ts",
    "content": "import { describe, test, expect, beforeEach, mock } from 'bun:test'\n\n// Mock the database utilities\nconst mockRun = mock()\nconst mockGet = mock()\nconst mockAll = mock()\n\n// Mock the utils module\nmock.module('./utils', () => ({\n  run: mockRun,\n  get: mockGet,\n  all: mockAll,\n}))\n\nimport { InteractionsTable, NotesTable, DictionaryTable } from './repo'\nimport { resetSqliteMocks } from '../../__tests__/mocks/sqlite'\nimport {\n  sampleInteraction,\n  TEST_USER_ID,\n} from '../../__tests__/fixtures/database'\n\n// Mock uuid\nmock.module('uuid', () => ({\n  v4: mock(() => 'test-uuid-123'),\n}))\n\ndescribe('InteractionsTable - Business Logic', () => {\n  beforeEach(() => {\n    mockRun.mockClear()\n    mockGet.mockClear()\n    mockAll.mockClear()\n    resetSqliteMocks()\n  })\n\n  describe('JSON field handling', () => {\n    test('should handle complex JSON objects in asr_output and llm_output', async () => {\n      const complexAsrOutput = {\n        transcript: 'complex audio',\n        words: [\n          { text: 'complex', start: 0, end: 1, confidence: 0.95 },\n          { text: 'audio', start: 1, end: 2, confidence: 0.88 },\n        ],\n        metadata: { sampleRate: 16000, channels: 1 },\n      }\n\n      const complexLlmOutput = {\n        response: 'Complex response',\n        confidence: 0.92,\n        tokens: ['Complex', 'response'],\n        model: 'test-model-v1',\n      }\n\n      const insertData = {\n        user_id: TEST_USER_ID,\n        title: 'Complex Interaction',\n        asr_output: complexAsrOutput,\n        llm_output: complexLlmOutput,\n        raw_audio: null,\n        duration_ms: 3000,\n      }\n\n      mockRun.mockResolvedValue(undefined)\n\n      const result = await InteractionsTable.insert(insertData)\n\n      expect(result.asr_output).toEqual(complexAsrOutput)\n      expect(result.llm_output).toEqual(complexLlmOutput)\n\n      // Verify JSON serialization in database call\n      expect(mockRun).toHaveBeenCalledWith(\n        expect.stringContaining('INSERT INTO interactions'),\n        expect.arrayContaining([\n          expect.any(String),\n          TEST_USER_ID,\n          'Complex Interaction',\n          JSON.stringify(complexAsrOutput),\n          JSON.stringify(complexLlmOutput),\n          null,\n          3000,\n          expect.any(String),\n          expect.any(String),\n          null,\n        ]),\n      )\n    })\n\n    test('should parse double-encoded JSON fields correctly', async () => {\n      const originalData = { transcript: 'double encoded test' }\n      const doubleEncodedAsr = JSON.stringify(JSON.stringify(originalData))\n\n      const mockInteraction = {\n        ...sampleInteraction,\n        asr_output: doubleEncodedAsr,\n        llm_output: JSON.stringify({ response: 'normal encoding' }),\n      }\n\n      mockGet.mockResolvedValue(mockInteraction)\n\n      const result = await InteractionsTable.findById('interaction-1')\n\n      expect(result!.asr_output).toEqual(originalData)\n      expect(result!.llm_output).toEqual({ response: 'normal encoding' })\n    })\n\n    test('should handle malformed JSON gracefully and log errors', async () => {\n      // Mock console.error to capture error logging\n      const consoleSpy = mock()\n      const originalError = console.error\n      console.error = consoleSpy\n\n      try {\n        const mockInteraction = {\n          ...sampleInteraction,\n          asr_output: 'invalid json {',\n          llm_output: JSON.stringify({ response: 'valid json' }),\n        }\n\n        mockGet.mockResolvedValue(mockInteraction)\n\n        const result = await InteractionsTable.findById('interaction-1')\n\n        // Should gracefully handle malformed JSON by returning null\n        expect(result!.asr_output).toBeNull()\n        expect(result!.llm_output).toEqual({ response: 'valid json' })\n\n        // Should log the error\n        expect(consoleSpy).toHaveBeenCalledWith(\n          '[InteractionsTable] Failed to parse JSON field:',\n          expect.any(Error),\n        )\n        expect(consoleSpy).toHaveBeenCalledTimes(1)\n      } finally {\n        console.error = originalError\n      }\n    })\n\n    test('should handle null and non-string JSON fields', async () => {\n      const mockInteraction = {\n        ...sampleInteraction,\n        asr_output: null,\n        llm_output: undefined,\n      }\n\n      mockGet.mockResolvedValue(mockInteraction)\n\n      const result = await InteractionsTable.findById('interaction-1')\n\n      expect(result!.asr_output).toBeNull()\n      expect(result!.llm_output).toBeUndefined()\n    })\n\n    test('should handle already parsed objects without re-parsing', async () => {\n      const alreadyParsedObject = { transcript: 'already parsed' }\n\n      const mockInteraction = {\n        ...sampleInteraction,\n        asr_output: alreadyParsedObject, // Not a string\n        llm_output: JSON.stringify({ response: 'string to parse' }),\n      }\n\n      mockGet.mockResolvedValue(mockInteraction)\n\n      const result = await InteractionsTable.findById('interaction-1')\n\n      expect(result!.asr_output).toEqual(alreadyParsedObject)\n      expect(result!.llm_output).toEqual({ response: 'string to parse' })\n    })\n  })\n\n  describe('data transformation', () => {\n    test('should generate UUID and timestamps for new interactions', async () => {\n      const insertData = {\n        user_id: TEST_USER_ID,\n        title: 'Test Interaction',\n        asr_output: { transcript: 'hello' },\n        llm_output: { response: 'hi' },\n        raw_audio: null,\n        duration_ms: 1000,\n      }\n\n      mockRun.mockResolvedValue(undefined)\n\n      const result = await InteractionsTable.insert(insertData)\n\n      // Should add generated fields\n      expect(result.id).toBe('test-uuid-123')\n      expect(result.created_at).toBeDefined()\n      expect(result.updated_at).toBeDefined()\n      expect(result.deleted_at).toBeNull()\n\n      // Should preserve input data\n      expect(result.user_id).toBe(TEST_USER_ID)\n      expect(result.title).toBe('Test Interaction')\n      expect(result.asr_output).toEqual({ transcript: 'hello' })\n      expect(result.llm_output).toEqual({ response: 'hi' })\n    })\n\n    test('should handle null values in insert data', async () => {\n      const insertData = {\n        user_id: null,\n        title: 'Anonymous Interaction',\n        asr_output: { transcript: 'anonymous' },\n        llm_output: { response: 'Response' },\n        raw_audio: null,\n        duration_ms: null,\n      }\n\n      mockRun.mockResolvedValue(undefined)\n\n      const result = await InteractionsTable.insert(insertData)\n\n      expect(result.user_id).toBeNull()\n      expect(result.raw_audio).toBeNull()\n      expect(result.duration_ms).toBeNull()\n      expect(result.id).toBeDefined()\n      expect(result.created_at).toBeDefined()\n    })\n  })\n})\n\ndescribe('NotesTable - Business Logic', () => {\n  beforeEach(() => {\n    mockRun.mockClear()\n    mockGet.mockClear()\n    mockAll.mockClear()\n  })\n\n  describe('content handling', () => {\n    test('should handle string content directly', async () => {\n      mockRun.mockResolvedValue(undefined)\n\n      await NotesTable.updateContent('note-1', 'Simple string content')\n\n      expect(mockRun).toHaveBeenCalledWith(\n        'UPDATE notes SET content = ?, updated_at = ? WHERE id = ?',\n        ['Simple string content', expect.any(String), 'note-1'],\n      )\n    })\n\n    test('should stringify object content', async () => {\n      mockRun.mockResolvedValue(undefined)\n      const objectContent = {\n        type: 'rich',\n        text: 'Rich content',\n        metadata: { author: 'user', version: 1 },\n      }\n\n      await NotesTable.updateContent('note-1', objectContent as any)\n\n      expect(mockRun).toHaveBeenCalledWith(\n        'UPDATE notes SET content = ?, updated_at = ? WHERE id = ?',\n        [JSON.stringify(objectContent), expect.any(String), 'note-1'],\n      )\n    })\n\n    test('should handle array content by stringifying', async () => {\n      mockRun.mockResolvedValue(undefined)\n      const arrayContent = ['item1', 'item2', 'item3']\n\n      await NotesTable.updateContent('note-1', arrayContent as any)\n\n      expect(mockRun).toHaveBeenCalledWith(\n        'UPDATE notes SET content = ?, updated_at = ? WHERE id = ?',\n        [JSON.stringify(arrayContent), expect.any(String), 'note-1'],\n      )\n    })\n\n    test('should update timestamp when updating content', async () => {\n      mockRun.mockResolvedValue(undefined)\n      const beforeTime = Date.now()\n\n      await NotesTable.updateContent('note-1', 'Updated content')\n\n      const call = mockRun.mock.calls[0]\n      const timestamp = call[1][1] // second parameter (updated_at)\n      const afterTime = Date.now()\n\n      // Verify timestamp is a valid ISO string within reasonable time range\n      expect(new Date(timestamp).getTime()).toBeGreaterThanOrEqual(beforeTime)\n      expect(new Date(timestamp).getTime()).toBeLessThanOrEqual(afterTime)\n    })\n  })\n\n  describe('data transformation', () => {\n    test('should generate UUID and timestamps for new notes', async () => {\n      const insertData = {\n        user_id: TEST_USER_ID,\n        interaction_id: 'interaction-1',\n        content: 'Test note content',\n      }\n\n      mockRun.mockResolvedValue(undefined)\n\n      const result = await NotesTable.insert(insertData)\n\n      expect(result.id).toBe('test-uuid-123')\n      expect(result.created_at).toBeDefined()\n      expect(result.updated_at).toBeDefined()\n      expect(result.deleted_at).toBeNull()\n      expect(result.user_id).toBe(TEST_USER_ID)\n      expect(result.interaction_id).toBe('interaction-1')\n      expect(result.content).toBe('Test note content')\n    })\n  })\n})\n\ndescribe('Timestamp Generation', () => {\n  test('should generate valid ISO timestamps', async () => {\n    const insertData = {\n      user_id: TEST_USER_ID,\n      title: 'Timestamp Test',\n      asr_output: {},\n      llm_output: {},\n      raw_audio: null,\n      duration_ms: null,\n    }\n\n    mockRun.mockResolvedValue(undefined)\n    const beforeTime = Date.now()\n\n    const result = await InteractionsTable.insert(insertData)\n\n    const afterTime = Date.now()\n\n    // Verify timestamps are valid ISO strings\n    expect(() => new Date(result.created_at)).not.toThrow()\n    expect(() => new Date(result.updated_at)).not.toThrow()\n\n    // Verify timestamps are within reasonable time range\n    const createdTime = new Date(result.created_at).getTime()\n    const updatedTime = new Date(result.updated_at).getTime()\n\n    expect(createdTime).toBeGreaterThanOrEqual(beforeTime)\n    expect(createdTime).toBeLessThanOrEqual(afterTime)\n    expect(updatedTime).toBeGreaterThanOrEqual(beforeTime)\n    expect(updatedTime).toBeLessThanOrEqual(afterTime)\n  })\n})\n\ndescribe('DictionaryTable - Business Logic', () => {\n  beforeEach(() => {\n    mockRun.mockClear()\n    mockGet.mockClear()\n    mockAll.mockClear()\n  })\n\n  describe('insert', () => {\n    test('should generate UUID for new dictionary item', async () => {\n      const insertData = {\n        user_id: TEST_USER_ID,\n        word: 'example',\n        pronunciation: 'ig-ZAM-pul',\n      }\n\n      mockRun.mockResolvedValue(undefined)\n\n      const result = await DictionaryTable.insert(insertData)\n\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.data.id).toBe('test-uuid-123')\n        expect(result.data.user_id).toBe(TEST_USER_ID)\n        expect(result.data.word).toBe('example')\n        expect(result.data.pronunciation).toBe('ig-ZAM-pul')\n      }\n    })\n\n    test('should set created_at and updated_at to sensible values', async () => {\n      const insertData = {\n        user_id: TEST_USER_ID,\n        word: 'timestamp',\n        pronunciation: 'TIME-stamp',\n      }\n\n      mockRun.mockResolvedValue(undefined)\n      const beforeTime = Date.now()\n\n      const result = await DictionaryTable.insert(insertData)\n\n      const afterTime = Date.now()\n\n      expect(result.success).toBe(true)\n      if (result.success) {\n        // Verify timestamps are defined and valid ISO strings\n        expect(result.data.created_at).toBeDefined()\n        expect(result.data.updated_at).toBeDefined()\n        expect(() => new Date(result.data.created_at)).not.toThrow()\n        expect(() => new Date(result.data.updated_at)).not.toThrow()\n\n        // Verify timestamps are within reasonable time range\n        const createdTime = new Date(result.data.created_at).getTime()\n        const updatedTime = new Date(result.data.updated_at).getTime()\n\n        expect(createdTime).toBeGreaterThanOrEqual(beforeTime)\n        expect(createdTime).toBeLessThanOrEqual(afterTime)\n        expect(updatedTime).toBeGreaterThanOrEqual(beforeTime)\n        expect(updatedTime).toBeLessThanOrEqual(afterTime)\n\n        // Verify deleted_at is null for new items\n        expect(result.data.deleted_at).toBeNull()\n      }\n    })\n\n    test('should preserve all input fields correctly', async () => {\n      const insertData = {\n        user_id: TEST_USER_ID,\n        word: 'preserve',\n        pronunciation: 'pri-ZURV',\n      }\n\n      mockRun.mockResolvedValue(undefined)\n\n      const result = await DictionaryTable.insert(insertData)\n\n      expect(result.success).toBe(true)\n      if (result.success) {\n        // Should preserve input data exactly\n        expect(result.data.user_id).toBe(TEST_USER_ID)\n        expect(result.data.word).toBe('preserve')\n        expect(result.data.pronunciation).toBe('pri-ZURV')\n\n        // Should add generated fields\n        expect(result.data.id).toBeDefined()\n        expect(result.data.created_at).toBeDefined()\n        expect(result.data.updated_at).toBeDefined()\n        expect(result.data.deleted_at).toBeNull()\n      }\n    })\n\n    test('should handle null pronunciation correctly', async () => {\n      const insertData = {\n        user_id: TEST_USER_ID,\n        word: 'simple',\n        pronunciation: null,\n      }\n\n      mockRun.mockResolvedValue(undefined)\n\n      const result = await DictionaryTable.insert(insertData)\n\n      expect(result.success).toBe(true)\n      if (result.success) {\n        expect(result.data.word).toBe('simple')\n        expect(result.data.pronunciation).toBeNull()\n        expect(result.data.id).toBeDefined()\n        expect(result.data.created_at).toBeDefined()\n      }\n    })\n\n    test('should handle unique constraint error correctly', async () => {\n      const insertData = {\n        user_id: TEST_USER_ID,\n        word: 'duplicate',\n        pronunciation: null,\n      }\n\n      // Mock unique constraint error\n      const constraintError = new Error('UNIQUE constraint failed')\n      constraintError.code = 'SQLITE_CONSTRAINT_UNIQUE'\n      mockRun.mockRejectedValue(constraintError)\n\n      const result = await DictionaryTable.insert(insertData)\n\n      expect(result.success).toBe(false)\n      if (!result.success) {\n        expect(result.error).toBe(\n          '\"duplicate\" already exists in your dictionary',\n        )\n        expect(result.errorType).toBe('DUPLICATE')\n      }\n    })\n\n    test('should handle general database error correctly', async () => {\n      const insertData = {\n        user_id: TEST_USER_ID,\n        word: 'error',\n        pronunciation: null,\n      }\n\n      // Mock general database error\n      const dbError = new Error('Database connection failed')\n      mockRun.mockRejectedValue(dbError)\n\n      const result = await DictionaryTable.insert(insertData)\n\n      expect(result.success).toBe(false)\n      if (!result.success) {\n        expect(result.error).toBe('Database connection failed')\n        expect(result.errorType).toBe('UNKNOWN')\n      }\n    })\n  })\n\n  describe('update', () => {\n    test('should call run with sensible updated_at timestamp', async () => {\n      mockRun.mockResolvedValue(undefined)\n      const beforeTime = Date.now()\n\n      const result = await DictionaryTable.update(\n        'dict-1',\n        'updated',\n        'up-DAY-ted',\n      )\n\n      const afterTime = Date.now()\n\n      expect(result.success).toBe(true)\n\n      // Verify the call was made with correct parameters\n      expect(mockRun).toHaveBeenCalledWith(\n        'UPDATE dictionary_items SET word = ?, pronunciation = ?, updated_at = ? WHERE id = ?',\n        ['updated', 'up-DAY-ted', expect.any(String), 'dict-1'],\n      )\n\n      // Extract and verify the timestamp\n      const call = mockRun.mock.calls[0]\n      const updatedAtTimestamp = call[1][2] // third parameter (updated_at)\n\n      // Verify it's a valid ISO string within reasonable time range\n      expect(() => new Date(updatedAtTimestamp)).not.toThrow()\n      const timestampTime = new Date(updatedAtTimestamp).getTime()\n      expect(timestampTime).toBeGreaterThanOrEqual(beforeTime)\n      expect(timestampTime).toBeLessThanOrEqual(afterTime)\n    })\n\n    test('should handle null pronunciation in update', async () => {\n      mockRun.mockResolvedValue(undefined)\n\n      const result = await DictionaryTable.update('dict-1', 'updated', null)\n\n      expect(result.success).toBe(true)\n\n      expect(mockRun).toHaveBeenCalledWith(\n        'UPDATE dictionary_items SET word = ?, pronunciation = ?, updated_at = ? WHERE id = ?',\n        ['updated', null, expect.any(String), 'dict-1'],\n      )\n    })\n\n    test('should update timestamp even with same word and pronunciation', async () => {\n      mockRun.mockResolvedValue(undefined)\n\n      const result = await DictionaryTable.update('dict-1', 'same', 'same')\n\n      expect(result.success).toBe(true)\n\n      const call = mockRun.mock.calls[0]\n      const updatedAtTimestamp = call[1][2]\n\n      // Should still generate a new timestamp\n      expect(updatedAtTimestamp).toBeDefined()\n      expect(() => new Date(updatedAtTimestamp)).not.toThrow()\n    })\n\n    test('should handle unique constraint error in update', async () => {\n      const constraintError = new Error('UNIQUE constraint failed')\n      constraintError.code = 'SQLITE_CONSTRAINT_UNIQUE'\n      mockRun.mockRejectedValue(constraintError)\n\n      const result = await DictionaryTable.update('dict-1', 'duplicate', null)\n\n      expect(result.success).toBe(false)\n      if (!result.success) {\n        expect(result.error).toBe(\n          '\"duplicate\" already exists in your dictionary',\n        )\n        expect(result.errorType).toBe('DUPLICATE')\n      }\n    })\n  })\n\n  describe('softDelete', () => {\n    test('should call run with sensible updated_at and deleted_at timestamps', async () => {\n      mockRun.mockResolvedValue(undefined)\n      const beforeTime = Date.now()\n\n      await DictionaryTable.softDelete('dict-1')\n\n      const afterTime = Date.now()\n\n      // Verify the call was made with correct SQL and parameter structure\n      expect(mockRun).toHaveBeenCalledWith(\n        'UPDATE dictionary_items SET deleted_at = ?, updated_at = ? WHERE id = ?',\n        [expect.any(String), expect.any(String), 'dict-1'],\n      )\n\n      // Extract and verify both timestamps\n      const call = mockRun.mock.calls[0]\n      const deletedAtTimestamp = call[1][0] // first parameter (deleted_at)\n      const updatedAtTimestamp = call[1][1] // second parameter (updated_at)\n\n      // Verify both are valid ISO strings within reasonable time range\n      expect(() => new Date(deletedAtTimestamp)).not.toThrow()\n      expect(() => new Date(updatedAtTimestamp)).not.toThrow()\n\n      const deletedAtTime = new Date(deletedAtTimestamp).getTime()\n      const updatedAtTime = new Date(updatedAtTimestamp).getTime()\n\n      expect(deletedAtTime).toBeGreaterThanOrEqual(beforeTime)\n      expect(deletedAtTime).toBeLessThanOrEqual(afterTime)\n      expect(updatedAtTime).toBeGreaterThanOrEqual(beforeTime)\n      expect(updatedAtTime).toBeLessThanOrEqual(afterTime)\n    })\n\n    test('updated_at and deleted_at should be the same', async () => {\n      mockRun.mockResolvedValue(undefined)\n\n      await DictionaryTable.softDelete('dict-1')\n\n      const call = mockRun.mock.calls[0]\n      const deletedAtTimestamp = call[1][0]\n      const updatedAtTimestamp = call[1][1]\n\n      const deletedAtTime = new Date(deletedAtTimestamp).getTime()\n      const updatedAtTime = new Date(updatedAtTimestamp).getTime()\n\n      expect(deletedAtTime).toBe(updatedAtTime)\n    })\n  })\n})\n"
  },
  {
    "path": "lib/main/sqlite/repo.ts",
    "content": "import { run, get, all } from './utils'\nimport type { Interaction, Note, DictionaryItem, UserMetadata } from './models'\nimport { PaidStatus } from './models'\nimport { v4 as uuidv4 } from 'uuid'\n\n// SQLite error codes (from better-sqlite3 and node-sqlite3)\nconst SQLITE_CONSTRAINT_UNIQUE = 'SQLITE_CONSTRAINT_UNIQUE'\n\n// Helper function to check if error is a unique constraint violation\nfunction isUniqueConstraintError(error: any): boolean {\n  return (\n    error.code === SQLITE_CONSTRAINT_UNIQUE ||\n    error.message?.includes('UNIQUE constraint failed') ||\n    error.errno === 19 // SQLITE_CONSTRAINT\n  )\n}\n\n// Result type for database operations\nexport type DbResult<T> =\n  | { success: true; data: T }\n  | { success: false; error: string; errorType?: string }\n\n// Helper function to handle unique constraint errors for dictionary items\nfunction handleDictionaryConstraintError(\n  error: any,\n  word: string,\n): DbResult<never> {\n  if (isUniqueConstraintError(error)) {\n    return {\n      success: false,\n      error: `\"${word}\" already exists in your dictionary`,\n      errorType: 'DUPLICATE',\n    }\n  }\n  return {\n    success: false,\n    error: error.message || 'Database operation failed',\n    errorType: 'UNKNOWN',\n  }\n}\n\n// Helper function to parse JSON fields and handle double encoding\nfunction parseJsonField(value: any): any {\n  if (!value || typeof value !== 'string') {\n    return value\n  }\n\n  try {\n    let parsed = JSON.parse(value)\n    // Check if it's double-encoded (parsed result is still a string)\n    if (typeof parsed === 'string') {\n      parsed = JSON.parse(parsed)\n    }\n    return parsed\n  } catch (error) {\n    console.error('[InteractionsTable] Failed to parse JSON field:', error)\n    return null\n  }\n}\n\n// Helper function to parse interaction JSON fields\nfunction parseInteractionJsonFields(interaction: Interaction): Interaction {\n  interaction.asr_output = parseJsonField(interaction.asr_output)\n  interaction.llm_output = parseJsonField(interaction.llm_output)\n  return interaction\n}\n\n// =================================================================\n// Interactions\n// =================================================================\n\n/**\n * Data required to create a new Interaction.\n * The repository will handle the rest of the fields.\n */\ntype InsertInteraction = Omit<\n  Interaction,\n  'id' | 'created_at' | 'updated_at' | 'deleted_at'\n>\n\nexport class InteractionsTable {\n  static async insert(\n    interactionData: InsertInteraction,\n  ): Promise<Interaction> {\n    const newInteraction: Interaction = {\n      id: uuidv4(),\n      ...interactionData,\n      created_at: new Date().toISOString(),\n      updated_at: new Date().toISOString(),\n      deleted_at: null,\n    }\n\n    const query = `\n      INSERT INTO interactions (id, user_id, title, asr_output, llm_output, raw_audio, raw_audio_id, duration_ms, sample_rate, created_at, updated_at, deleted_at)\n      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n    `\n    // Note: SQLite doesn't have a dedicated JSON type, so we stringify complex objects\n    const params = [\n      newInteraction.id,\n      newInteraction.user_id,\n      newInteraction.title,\n      JSON.stringify(newInteraction.asr_output),\n      JSON.stringify(newInteraction.llm_output),\n      newInteraction.raw_audio,\n      newInteraction.raw_audio_id,\n      newInteraction.duration_ms,\n      newInteraction.sample_rate,\n      newInteraction.created_at,\n      newInteraction.updated_at,\n      newInteraction.deleted_at,\n    ]\n\n    await run(query, params)\n    return newInteraction\n  }\n\n  static async findById(id: string): Promise<Interaction | undefined> {\n    const row = await get<Interaction>(\n      'SELECT * FROM interactions WHERE id = ?',\n      [id],\n    )\n    return row ? parseInteractionJsonFields(row) : undefined\n  }\n\n  static async findAll(user_id?: string): Promise<Interaction[]> {\n    const query = user_id\n      ? 'SELECT * FROM interactions WHERE user_id = ? AND deleted_at IS NULL ORDER BY created_at DESC'\n      : 'SELECT * FROM interactions WHERE user_id IS NULL AND deleted_at IS NULL ORDER BY created_at DESC'\n    const params = user_id ? [user_id] : []\n    const rows = await all<Interaction>(query, params)\n\n    return rows.map(parseInteractionJsonFields)\n  }\n\n  static async softDelete(id: string): Promise<void> {\n    const query =\n      'UPDATE interactions SET deleted_at = ?, updated_at = ? WHERE id = ?'\n    await run(query, [new Date().toISOString(), new Date().toISOString(), id])\n  }\n\n  static async deleteAllUserData(userId: string): Promise<void> {\n    const query =\n      'UPDATE interactions SET deleted_at = ?, updated_at = ? WHERE user_id = ?'\n    await run(query, [\n      new Date().toISOString(),\n      new Date().toISOString(),\n      userId,\n    ])\n  }\n\n  static async findModifiedSince(timestamp: string): Promise<Interaction[]> {\n    const rows = await all<Interaction>(\n      'SELECT * FROM interactions WHERE updated_at > ?',\n      [timestamp],\n    )\n\n    return rows.map(parseInteractionJsonFields)\n  }\n\n  static async upsert(interaction: Interaction): Promise<void> {\n    const query = `\n      INSERT INTO interactions (id, user_id, title, asr_output, llm_output, raw_audio, duration_ms, sample_rate, created_at, updated_at, deleted_at)\n      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n      ON CONFLICT(id) DO UPDATE SET\n        title = excluded.title,\n        asr_output = excluded.asr_output,\n        llm_output = excluded.llm_output,\n        raw_audio = excluded.raw_audio,\n        duration_ms = excluded.duration_ms,\n        sample_rate = excluded.sample_rate,\n        updated_at = excluded.updated_at,\n        deleted_at = excluded.deleted_at;\n    `\n    const params = [\n      interaction.id,\n      interaction.user_id,\n      interaction.title,\n      JSON.stringify(interaction.asr_output),\n      JSON.stringify(interaction.llm_output),\n      interaction.raw_audio,\n      interaction.duration_ms,\n      interaction.sample_rate,\n      interaction.created_at,\n      interaction.updated_at,\n      interaction.deleted_at,\n    ]\n\n    await run(query, params)\n  }\n}\n\n// =================================================================\n// Notes\n// =================================================================\n\n/**\n * Data required to create a new Note.\n */\ntype InsertNote = Omit<Note, 'id' | 'created_at' | 'updated_at' | 'deleted_at'>\n\nexport class NotesTable {\n  static async insert(noteData: InsertNote): Promise<Note> {\n    const newNote: Note = {\n      id: uuidv4(),\n      ...noteData,\n      created_at: new Date().toISOString(),\n      updated_at: new Date().toISOString(),\n      deleted_at: null,\n    }\n\n    const query = `\n            INSERT INTO notes (id, user_id, interaction_id, content, created_at, updated_at, deleted_at)\n            VALUES (?, ?, ?, ?, ?, ?, ?)\n        `\n    const params = [\n      newNote.id,\n      newNote.user_id,\n      newNote.interaction_id,\n      newNote.content,\n      newNote.created_at,\n      newNote.updated_at,\n      newNote.deleted_at,\n    ]\n\n    await run(query, params)\n    return newNote\n  }\n\n  static async findById(id: string): Promise<Note | undefined> {\n    return await get<Note>('SELECT * FROM notes WHERE id = ?', [id])\n  }\n\n  static async findAll(user_id?: string): Promise<Note[]> {\n    const query = user_id\n      ? 'SELECT * FROM notes WHERE user_id = ? AND deleted_at IS NULL ORDER BY created_at DESC'\n      : 'SELECT * FROM notes WHERE user_id IS NULL AND deleted_at IS NULL ORDER BY created_at DESC'\n    const params = user_id ? [user_id] : []\n    return await all<Note>(query, params)\n  }\n\n  static async findByInteractionId(interactionId: string): Promise<Note[]> {\n    return await all<Note>(\n      'SELECT * FROM notes WHERE interaction_id = ? AND deleted_at IS NULL ORDER BY created_at ASC',\n      [interactionId],\n    )\n  }\n\n  static async updateContent(id: string, content: string): Promise<void> {\n    const query = 'UPDATE notes SET content = ?, updated_at = ? WHERE id = ?'\n    await run(query, [\n      typeof content === 'string' ? content : JSON.stringify(content),\n      new Date().toISOString(),\n      id,\n    ])\n  }\n\n  static async softDelete(id: string): Promise<void> {\n    console.log('softDelete', id)\n    const query = 'UPDATE notes SET deleted_at = ?, updated_at = ? WHERE id = ?'\n    await run(query, [new Date().toISOString(), new Date().toISOString(), id])\n  }\n\n  static async deleteAllUserData(userId: string): Promise<void> {\n    const query =\n      'UPDATE notes SET deleted_at = ?, updated_at = ? WHERE user_id = ?'\n    await run(query, [\n      new Date().toISOString(),\n      new Date().toISOString(),\n      userId,\n    ])\n  }\n\n  static async findModifiedSince(timestamp: string): Promise<Note[]> {\n    return await all<Note>('SELECT * FROM notes WHERE updated_at > ?', [\n      timestamp,\n    ])\n  }\n\n  static async upsert(note: Note): Promise<void> {\n    const query = `\n      INSERT INTO notes (id, user_id, interaction_id, content, created_at, updated_at, deleted_at)\n      VALUES (?, ?, ?, ?, ?, ?, ?)\n      ON CONFLICT(id) DO UPDATE SET\n        interaction_id = excluded.interaction_id,\n        content = excluded.content,\n        updated_at = excluded.updated_at,\n        deleted_at = excluded.deleted_at;\n    `\n    const params = [\n      note.id,\n      note.user_id,\n      note.interaction_id,\n      note.content,\n      note.created_at,\n      note.updated_at,\n      note.deleted_at,\n    ]\n    await run(query, params)\n  }\n}\n\n// =================================================================\n// Dictionary\n// =================================================================\n\n/**\n * Data required to create a new Dictionary Item.\n */\ntype InsertDictionaryItem = Omit<\n  DictionaryItem,\n  'id' | 'created_at' | 'updated_at' | 'deleted_at'\n>\n\nexport class DictionaryTable {\n  static async insert(\n    itemData: InsertDictionaryItem,\n  ): Promise<DbResult<DictionaryItem>> {\n    const newItem: DictionaryItem = {\n      id: uuidv4(),\n      ...itemData,\n      created_at: new Date().toISOString(),\n      updated_at: new Date().toISOString(),\n      deleted_at: null,\n    }\n\n    const query = `\n            INSERT INTO dictionary_items (id, user_id, word, pronunciation, created_at, updated_at, deleted_at)\n            VALUES (?, ?, ?, ?, ?, ?, ?)\n        `\n    const params = [\n      newItem.id,\n      newItem.user_id,\n      newItem.word,\n      newItem.pronunciation,\n      newItem.created_at,\n      newItem.updated_at,\n      newItem.deleted_at,\n    ]\n\n    try {\n      await run(query, params)\n      return { success: true, data: newItem }\n    } catch (error: any) {\n      return handleDictionaryConstraintError(error, newItem.word)\n    }\n  }\n\n  static async findAll(user_id?: string): Promise<DictionaryItem[]> {\n    const query = user_id\n      ? 'SELECT * FROM dictionary_items WHERE user_id = ? AND deleted_at IS NULL ORDER BY word ASC'\n      : 'SELECT * FROM dictionary_items WHERE user_id IS NULL AND deleted_at IS NULL ORDER BY word ASC'\n    const params = user_id ? [user_id] : []\n    return await all<DictionaryItem>(query, params)\n  }\n\n  static async update(\n    id: string,\n    word: string,\n    pronunciation: string | null,\n  ): Promise<DbResult<void>> {\n    const query =\n      'UPDATE dictionary_items SET word = ?, pronunciation = ?, updated_at = ? WHERE id = ?'\n    try {\n      await run(query, [word, pronunciation, new Date().toISOString(), id])\n      return { success: true, data: undefined }\n    } catch (error: any) {\n      return handleDictionaryConstraintError(error, word)\n    }\n  }\n\n  static async softDelete(id: string): Promise<void> {\n    const now = new Date().toISOString()\n    const query =\n      'UPDATE dictionary_items SET deleted_at = ?, updated_at = ? WHERE id = ?'\n    await run(query, [now, now, id])\n  }\n\n  static async deleteAllUserData(userId: string): Promise<void> {\n    const now = new Date().toISOString()\n    const query =\n      'UPDATE dictionary_items SET deleted_at = ?, updated_at = ? WHERE user_id = ?'\n    await run(query, [now, now, userId])\n  }\n\n  static async findModifiedSince(timestamp: string): Promise<DictionaryItem[]> {\n    return await all<DictionaryItem>(\n      'SELECT * FROM dictionary_items WHERE updated_at > ?',\n      [timestamp],\n    )\n  }\n\n  static async upsert(item: DictionaryItem): Promise<DbResult<void>> {\n    const query = `\n      INSERT INTO dictionary_items (id, user_id, word, pronunciation, created_at, updated_at, deleted_at)\n      VALUES (?, ?, ?, ?, ?, ?, ?)\n      ON CONFLICT(id) DO UPDATE SET\n        word = excluded.word,\n        pronunciation = excluded.pronunciation,\n        updated_at = excluded.updated_at,\n        deleted_at = excluded.deleted_at;\n    `\n    const params = [\n      item.id,\n      item.user_id,\n      item.word,\n      item.pronunciation,\n      item.created_at,\n      item.updated_at,\n      item.deleted_at,\n    ]\n    try {\n      await run(query, params)\n      return { success: true, data: undefined }\n    } catch (error: any) {\n      return handleDictionaryConstraintError(error, item.word)\n    }\n  }\n}\n\n// =================================================================\n// KeyValueStore\n// =================================================================\n\nexport class KeyValueStore {\n  static async set(key: string, value: string): Promise<void> {\n    const query = `\n      INSERT INTO key_value_store (key, value)\n      VALUES (?, ?)\n      ON CONFLICT(key) DO UPDATE SET value = excluded.value;\n    `\n    await run(query, [key, value])\n  }\n\n  static async get(key: string): Promise<string | undefined> {\n    const row = await get<{ value: string }>(\n      'SELECT value FROM key_value_store WHERE key = ?',\n      [key],\n    )\n    return row?.value\n  }\n\n  static async delete(key: string): Promise<void> {\n    const query = 'DELETE FROM key_value_store WHERE key = ?'\n    await run(query, [key])\n  }\n}\n\n// =================================================================\n// UserMetadata\n// =================================================================\n\n/**\n * Raw UserMetadata row from SQLite (dates are strings).\n */\ntype UserMetadataRow = {\n  id: string\n  user_id: string\n  paid_status: PaidStatus\n  free_words_remaining: number | null\n  pro_trial_start_date: string | null\n  pro_trial_end_date: string | null\n  pro_subscription_start_date: string | null\n  pro_subscription_end_date: string | null\n  created_at: string\n  updated_at: string\n}\n\n/**\n * Transforms a raw DB row to UserMetadata with Date objects.\n */\nfunction parseUserMetadataRow(row: UserMetadataRow): UserMetadata {\n  return {\n    ...row,\n    pro_trial_start_date: row.pro_trial_start_date\n      ? new Date(row.pro_trial_start_date)\n      : null,\n    pro_trial_end_date: row.pro_trial_end_date\n      ? new Date(row.pro_trial_end_date)\n      : null,\n    pro_subscription_start_date: row.pro_subscription_start_date\n      ? new Date(row.pro_subscription_start_date)\n      : null,\n    pro_subscription_end_date: row.pro_subscription_end_date\n      ? new Date(row.pro_subscription_end_date)\n      : null,\n    created_at: new Date(row.created_at),\n    updated_at: new Date(row.updated_at),\n  }\n}\n\n/**\n * Data required to create or update UserMetadata.\n */\ntype InsertUserMetadata = Omit<UserMetadata, 'id' | 'created_at' | 'updated_at'>\n\nexport class UserMetadataTable {\n  static async insert(metadataData: InsertUserMetadata): Promise<UserMetadata> {\n    const now = new Date()\n    const newMetadata: UserMetadata = {\n      id: uuidv4(),\n      ...metadataData,\n      created_at: now,\n      updated_at: now,\n    }\n\n    const query = `\n      INSERT INTO user_metadata (\n        id, user_id, paid_status, free_words_remaining,\n        pro_trial_start_date, pro_trial_end_date,\n        pro_subscription_start_date, pro_subscription_end_date,\n        created_at, updated_at\n      )\n      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n    `\n    const params = [\n      newMetadata.id,\n      newMetadata.user_id,\n      newMetadata.paid_status,\n      newMetadata.free_words_remaining,\n      newMetadata.pro_trial_start_date?.toISOString() ?? null,\n      newMetadata.pro_trial_end_date?.toISOString() ?? null,\n      newMetadata.pro_subscription_start_date?.toISOString() ?? null,\n      newMetadata.pro_subscription_end_date?.toISOString() ?? null,\n      newMetadata.created_at.toISOString(),\n      newMetadata.updated_at.toISOString(),\n    ]\n\n    await run(query, params)\n    return newMetadata\n  }\n\n  static async findByUserId(userId: string): Promise<UserMetadata | undefined> {\n    const row = await get<UserMetadataRow>(\n      'SELECT * FROM user_metadata WHERE user_id = ?',\n      [userId],\n    )\n    return row ? parseUserMetadataRow(row) : undefined\n  }\n\n  /**\n   * Allows updating specific fields of user metadata. If\n   * the field is not provided, it will remain unchanged.\n   */\n  static async update(\n    userId: string,\n    updates: Partial<Omit<UserMetadata, 'id' | 'user_id' | 'created_at'>>,\n  ): Promise<void> {\n    const fields: string[] = []\n    const params: any[] = []\n\n    if (updates.paid_status !== undefined) {\n      fields.push('paid_status = ?')\n      params.push(updates.paid_status)\n    }\n    if (updates.free_words_remaining !== undefined) {\n      fields.push('free_words_remaining = ?')\n      params.push(updates.free_words_remaining)\n    }\n    if (updates.pro_trial_start_date !== undefined) {\n      fields.push('pro_trial_start_date = ?')\n      params.push(updates.pro_trial_start_date?.toISOString() ?? null)\n    }\n    if (updates.pro_trial_end_date !== undefined) {\n      fields.push('pro_trial_end_date = ?')\n      params.push(updates.pro_trial_end_date?.toISOString() ?? null)\n    }\n    if (updates.pro_subscription_start_date !== undefined) {\n      fields.push('pro_subscription_start_date = ?')\n      params.push(updates.pro_subscription_start_date?.toISOString() ?? null)\n    }\n    if (updates.pro_subscription_end_date !== undefined) {\n      fields.push('pro_subscription_end_date = ?')\n      params.push(updates.pro_subscription_end_date?.toISOString() ?? null)\n    }\n\n    fields.push('updated_at = ?')\n    params.push(new Date().toISOString())\n\n    params.push(userId)\n\n    const query = `UPDATE user_metadata SET ${fields.join(', ')} WHERE user_id = ?`\n    await run(query, params)\n  }\n\n  static async upsert(metadata: UserMetadata): Promise<void> {\n    const query = `\n      INSERT INTO user_metadata (\n        id, user_id, paid_status, free_words_remaining,\n        pro_trial_start_date, pro_trial_end_date,\n        pro_subscription_start_date, pro_subscription_end_date,\n        created_at, updated_at\n      )\n      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n      ON CONFLICT(user_id) DO UPDATE SET\n        paid_status = excluded.paid_status,\n        free_words_remaining = excluded.free_words_remaining,\n        pro_trial_start_date = excluded.pro_trial_start_date,\n        pro_trial_end_date = excluded.pro_trial_end_date,\n        pro_subscription_start_date = excluded.pro_subscription_start_date,\n        pro_subscription_end_date = excluded.pro_subscription_end_date,\n        updated_at = excluded.updated_at;\n    `\n    const params = [\n      metadata.id,\n      metadata.user_id,\n      metadata.paid_status,\n      metadata.free_words_remaining,\n      metadata.pro_trial_start_date?.toISOString() ?? null,\n      metadata.pro_trial_end_date?.toISOString() ?? null,\n      metadata.pro_subscription_start_date?.toISOString() ?? null,\n      metadata.pro_subscription_end_date?.toISOString() ?? null,\n      metadata.created_at.toISOString(),\n      metadata.updated_at.toISOString(),\n    ]\n\n    await run(query, params)\n  }\n\n  static async deleteByUserId(userId: string): Promise<void> {\n    const query = 'DELETE FROM user_metadata WHERE user_id = ?'\n    await run(query, [userId])\n  }\n}\n"
  },
  {
    "path": "lib/main/sqlite/schema.ts",
    "content": "export const INITIAL_SCHEMA = `\n  CREATE TABLE interactions (\n    id TEXT PRIMARY KEY,\n    user_id TEXT,\n    title TEXT,\n    asr_output TEXT,\n    llm_output TEXT,\n    created_at TEXT NOT NULL,\n    updated_at TEXT NOT NULL,\n    deleted_at TEXT\n  );\n\n  CREATE TABLE notes (\n    id TEXT PRIMARY KEY,\n    user_id TEXT NOT NULL,\n    interaction_id TEXT,\n    content TEXT NOT NULL,\n    created_at TEXT NOT NULL,\n    updated_at TEXT NOT NULL,\n    deleted_at TEXT,\n    FOREIGN KEY (interaction_id) REFERENCES interactions (id) ON DELETE SET NULL\n  );\n\n  CREATE TABLE dictionary_items (\n    id TEXT PRIMARY KEY,\n    user_id TEXT NOT NULL,\n    word TEXT NOT NULL,\n    pronunciation TEXT,\n    created_at TEXT NOT NULL,\n    updated_at TEXT NOT NULL,\n    deleted_at TEXT\n  );\n\n  CREATE TABLE key_value_store (\n    key TEXT PRIMARY KEY,\n    value TEXT\n  );\n`\n"
  },
  {
    "path": "lib/main/sqlite/utils.ts",
    "content": "import { getDb } from './db'\n\nexport const run = (query: string, params: any[] = []): Promise<void> =>\n  new Promise((resolve, reject) => {\n    getDb().run(query, params, function (err) {\n      if (err) return reject(err)\n      resolve()\n    })\n  })\n\nexport const exec = (query: string): Promise<void> =>\n  new Promise((resolve, reject) => {\n    getDb().exec(query, function (err) {\n      if (err) return reject(err)\n      resolve()\n    })\n  })\n\nexport const get = <T>(\n  query: string,\n  params: any[] = [],\n): Promise<T | undefined> =>\n  new Promise((resolve, reject) => {\n    getDb().get(query, params, (err, row: T) => {\n      if (err) return reject(err)\n      resolve(row)\n    })\n  })\n\nexport const all = <T>(query: string, params: any[] = []): Promise<T[]> =>\n  new Promise((resolve, reject) => {\n    getDb().all(query, params, (err, rows: T[]) => {\n      if (err) return reject(err)\n      resolve(rows)\n    })\n  })\n"
  },
  {
    "path": "lib/main/store.test.ts",
    "content": "import { describe, test, expect, beforeEach, mock } from 'bun:test'\n\n// Mock crypto module, preserving Node's built-ins used by dependencies like `uuid`\nconst realCrypto = require('node:crypto')\nconst mockCryptoPartial = {\n  ...realCrypto,\n  randomBytes: mock((_size: number) => ({\n    toString: mock((encoding: string) => {\n      if (encoding === 'base64url') return 'mock-base64url-string'\n      if (encoding === 'hex') return 'mock-hex-string'\n      return 'mock-string'\n    }),\n  })),\n  createHash: mock(() => ({\n    update: mock(() => ({\n      digest: mock(() => 'mock-hash-digest'),\n    })),\n  })),\n  randomUUID: mock(() => 'mock-uuid-123'),\n}\n\nconst mockCrypto = mockCryptoPartial\n\nmock.module('crypto', () => ({\n  default: mockCryptoPartial,\n  ...mockCryptoPartial,\n}))\n\n// Mock console to avoid noise\nbeforeEach(() => {\n  console.log = mock()\n  console.error = mock()\n})\n\ndescribe('KV-backed Store', () => {\n  beforeEach(() => {\n    // Clear module cache for fresh imports\n    delete require.cache[require.resolve('./store')]\n  })\n\n  test('should expose default values on first load', async () => {\n    const { default: store } = await import('./store')\n    const settings = store.get('settings')\n    expect(settings.shareAnalytics).toBe(true)\n    expect(settings.launchAtLogin).toBe(true)\n    expect(settings.isShortcutGloballyEnabled).toBe(false)\n    const main = store.get('main')\n    expect(main.navExpanded).toBe(true)\n  })\n\n  test('dot-path set should update nested value', async () => {\n    const { default: store } = await import('./store')\n    store.set('settings.launchAtLogin', false)\n    const settings = store.get('settings')\n    expect(settings.launchAtLogin).toBe(false)\n  })\n\n  test('delete should clear top-level key', async () => {\n    const { default: store } = await import('./store')\n    store.set('main', { navExpanded: false })\n    expect(store.get('main').navExpanded).toBe(false)\n    store.delete('main')\n    expect(store.get('main')).toBeUndefined()\n  })\n})\n\ndescribe('Auth helpers', () => {\n  test('getCurrentUserId should read from userProfile', async () => {\n    const { default: store, getCurrentUserId } = await import('./store')\n    store.set('userProfile', { id: 'user-123', name: 'T' })\n    expect(getCurrentUserId()).toBe('user-123')\n  })\n\n  test('createNewAuthState should use crypto functions', async () => {\n    const { createNewAuthState } = await import('./store')\n    const s = createNewAuthState()\n    expect(mockCrypto.randomBytes).toHaveBeenCalledWith(32)\n    expect(mockCrypto.randomBytes).toHaveBeenCalledWith(16)\n    expect(mockCrypto.createHash).toHaveBeenCalledWith('sha256')\n    expect(mockCrypto.randomUUID).toHaveBeenCalled()\n    expect(s).toEqual({\n      id: 'mock-uuid-123',\n      codeVerifier: 'mock-base64url-string',\n      codeChallenge: 'mock-hash-digest',\n      state: 'mock-hex-string',\n    })\n  })\n})\n"
  },
  {
    "path": "lib/main/store.ts",
    "content": "import crypto from 'crypto'\nimport { STORE_KEYS } from '../constants/store-keys'\nimport type { LlmSettings } from '@/app/store/useAdvancedSettingsStore'\nimport { ItoMode } from '@/app/generated/ito_pb.js'\nimport { ITO_MODE_SHORTCUT_DEFAULTS } from '../constants/keyboard-defaults.js'\nimport { KeyName, normalizeLegacyKey } from '../types/keyboard.js'\nimport { KeyValueStore } from './sqlite/repo'\nimport { resolveDefaultKeys } from '../utils/settings.js'\n\nexport interface KeyboardShortcutConfig {\n  id: string\n  keys: KeyName[]\n  mode: ItoMode\n}\n\ninterface MainStore {\n  navExpanded: boolean\n}\ninterface OnboardingStore {\n  onboardingStep: number\n  onboardingCompleted: boolean\n}\n\nexport interface SettingsStore {\n  shareAnalytics: boolean\n  launchAtLogin: boolean\n  showItoBarAlways: boolean\n  showAppInDock: boolean\n  interactionSounds: boolean\n  muteAudioWhenDictating: boolean\n  microphoneDeviceId: string\n  microphoneName: string\n  isShortcutGloballyEnabled: boolean\n  keyboardShortcuts: KeyboardShortcutConfig[]\n  firstName: string\n  lastName: string\n  email: string\n}\n\nexport interface AuthState {\n  id: string\n  codeVerifier: string\n  codeChallenge: string\n  state: string\n}\n\nexport interface AuthUser {\n  id: string\n  email?: string\n  name?: string\n  picture?: string\n  provider?: string\n  lastSignInAt?: string\n}\nexport interface AuthTokens {\n  access_token?: string\n  refresh_token?: string\n  id_token?: string\n  token_type?: string\n  expires_in?: number\n  expires_at?: number\n}\n\nexport interface AuthStore {\n  user: AuthUser | null\n  tokens: AuthTokens | null\n  state: AuthState\n}\n\nexport interface AdvancedSettings {\n  llm: LlmSettings\n  grammarServiceEnabled: boolean\n  defaults?: LlmSettings\n  macosAccessibilityContextEnabled: boolean\n}\n\ninterface AppStore {\n  main: MainStore\n  onboarding: OnboardingStore\n  settings: SettingsStore\n  auth: AuthStore\n  advancedSettings: AdvancedSettings\n  openMic: boolean\n  selectedAudioInput: string | null\n  interactionSounds: boolean\n  userProfile: any | null\n  idToken: string | null\n  accessToken: string | null\n  appliedMigrations: string[]\n}\n\nexport const createNewAuthState = (): AuthState => {\n  const codeVerifier = crypto.randomBytes(32).toString('base64url')\n  const codeChallenge = crypto\n    .createHash('sha256')\n    .update(codeVerifier)\n    .digest('base64url')\n  const state = crypto.randomBytes(16).toString('hex')\n  const id = crypto.randomUUID()\n  return { id, codeVerifier, codeChallenge, state }\n}\n\nexport const getCurrentUserId = (): string | undefined => {\n  const user = store.get(STORE_KEYS.USER_PROFILE) as any\n  return user?.id\n}\nexport const getAdvancedSettings = (): AdvancedSettings => {\n  const storeSettings = store.get(\n    STORE_KEYS.ADVANCED_SETTINGS,\n  ) as AdvancedSettings\n  return { ...storeSettings }\n}\n\nexport const defaultValues: AppStore = {\n  onboarding: { onboardingStep: 0, onboardingCompleted: false },\n  settings: {\n    shareAnalytics: true,\n    launchAtLogin: true,\n    showItoBarAlways: true,\n    showAppInDock: true,\n    interactionSounds: false,\n    muteAudioWhenDictating: false,\n    microphoneDeviceId: 'default',\n    microphoneName: 'Auto-detect',\n    isShortcutGloballyEnabled: false,\n    keyboardShortcuts: [\n      {\n        id: crypto.randomUUID(),\n        keys: ITO_MODE_SHORTCUT_DEFAULTS[ItoMode.TRANSCRIBE].map(\n          normalizeLegacyKey,\n        ) as KeyName[],\n        mode: ItoMode.TRANSCRIBE,\n      },\n      {\n        id: crypto.randomUUID(),\n        keys: ITO_MODE_SHORTCUT_DEFAULTS[ItoMode.EDIT].map(\n          normalizeLegacyKey,\n        ) as KeyName[],\n        mode: ItoMode.EDIT,\n      },\n    ],\n    firstName: '',\n    lastName: '',\n    email: '',\n  },\n  main: { navExpanded: true },\n  auth: { user: null, tokens: null, state: createNewAuthState() },\n  advancedSettings: {\n    grammarServiceEnabled: false,\n    macosAccessibilityContextEnabled: false,\n    llm: {\n      asrProvider: null,\n      asrModel: null,\n      asrPrompt: null,\n      llmProvider: null,\n      llmTemperature: null,\n      llmModel: null,\n      transcriptionPrompt: null,\n      editingPrompt: null,\n      noSpeechThreshold: null,\n    },\n  },\n  openMic: false,\n  selectedAudioInput: null,\n  interactionSounds: false,\n  userProfile: null,\n  idToken: null,\n  accessToken: null,\n  appliedMigrations: [],\n}\n\n// Lightweight store-like interface used for migrations and defaults logic\ntype StoreLike<T = any> = {\n  get: (path: string) => any\n  set: (path: string, value: any) => void\n}\n\n// In-memory cache that backs synchronous reads and dot-path writes\nconst cache: Record<string, any> = {\n  [STORE_KEYS.MAIN]: defaultValues.main,\n  [STORE_KEYS.ONBOARDING]: defaultValues.onboarding,\n  [STORE_KEYS.SETTINGS]: defaultValues.settings,\n  [STORE_KEYS.AUTH]: defaultValues.auth,\n  [STORE_KEYS.ADVANCED_SETTINGS]: defaultValues.advancedSettings,\n  [STORE_KEYS.OPEN_MIC]: defaultValues.openMic,\n  [STORE_KEYS.SELECTED_AUDIO_INPUT]: defaultValues.selectedAudioInput,\n  [STORE_KEYS.INTERACTION_SOUNDS]: defaultValues.interactionSounds,\n  [STORE_KEYS.USER_PROFILE]: defaultValues.userProfile,\n  [STORE_KEYS.ID_TOKEN]: defaultValues.idToken,\n  [STORE_KEYS.ACCESS_TOKEN]: defaultValues.accessToken,\n  appliedMigrations: defaultValues.appliedMigrations,\n}\n\nconst isObject = (v: any) =>\n  v !== null && typeof v === 'object' && !Array.isArray(v)\n\nfunction deepGet(obj: any, pathParts: string[]): any {\n  return pathParts.reduce(\n    (acc, part) => (acc == null ? undefined : acc[part]),\n    obj,\n  )\n}\n\nfunction deepSet(obj: any, pathParts: string[], value: any): any {\n  if (pathParts.length === 0) return value\n  const [head, ...rest] = pathParts\n  const target = isObject(obj) ? obj : {}\n  return {\n    ...target,\n    [head]: rest.length === 0 ? value : deepSet(target[head], rest, value),\n  }\n}\n\nasync function persistTopLevelKey(key: string) {\n  try {\n    await KeyValueStore.set(key, JSON.stringify(cache[key]))\n  } catch (err) {\n    console.error('[store] Failed to persist key', key, err)\n  }\n}\n\nexport const store: StoreLike<AppStore> & {\n  delete: (key: string) => void\n} = {\n  get: (path: string) => {\n    if (!path || typeof path !== 'string') return undefined\n    if (path.includes('.')) {\n      const [top, ...rest] = path.split('.')\n      const topVal = cache[top]\n      return deepGet(topVal, rest)\n    }\n    return cache[path]\n  },\n  set: (path: string, value: any) => {\n    if (!path || typeof path !== 'string') return\n    if (path.includes('.')) {\n      const [top, ...rest] = path.split('.')\n      const current = cache[top]\n      cache[top] = deepSet(current, rest, value)\n      void persistTopLevelKey(top)\n      return\n    }\n    cache[path] = value\n    void persistTopLevelKey(path)\n  },\n  delete: (key: string) => {\n    if (!key || typeof key !== 'string') return\n    delete cache[key]\n    KeyValueStore.delete(key).catch(err =>\n      console.error('[store] Failed to delete key', key, err),\n    )\n  },\n}\n\ntype Migration = { id: string; run: (s: StoreLike<AppStore>) => void }\n\nconst migrations: Migration[] = [\n  {\n    id: '2025-08-15-keyboard-shortcut-rename',\n    run: s => {\n      const settings: any = s.get('settings') || {}\n      const legacy = settings.keyboardShortcut\n      const hasLegacy = Array.isArray(legacy) && legacy.length > 0\n      const hasNew =\n        Array.isArray(settings.keyboardShortcuts) &&\n        settings.keyboardShortcuts.length > 0\n\n      if (!hasNew && hasLegacy) {\n        s.set('settings.keyboardShortcuts', [\n          {\n            id: crypto.randomUUID(),\n            keys: legacy,\n            mode: ItoMode.TRANSCRIBE,\n          },\n        ])\n      }\n      if ('keyboardShortcut' in settings) {\n        delete settings.keyboardShortcut\n        s.set('settings', settings)\n      }\n    },\n  },\n]\n\n// ---------- Migration runner ----------\nfunction runMigrations(s: StoreLike<AppStore>, allMigrations: Migration[]) {\n  const applied = new Set(s.get('appliedMigrations') || [])\n  for (const m of allMigrations) {\n    if (!applied.has(m.id)) {\n      console.log(`[migrations] Running: ${m.id}`)\n      try {\n        m.run(s)\n        applied.add(m.id)\n      } catch (err) {\n        console.error(`[migrations] Failed: ${m.id}`, err)\n      }\n    }\n  }\n  s.set('appliedMigrations', Array.from(applied))\n}\n\nfunction ensureDefaultsDeep<T = unknown>(\n  s: StoreLike<any>,\n  defaults: T,\n  basePath = '',\n  exclude: Set<string> = new Set(['appliedMigrations']), // skip internal/meta keys\n) {\n  const isObj = (v: any) =>\n    v !== null && typeof v === 'object' && !Array.isArray(v)\n\n  for (const [key, defaultValue] of Object.entries(defaults as any)) {\n    if (exclude.has(key)) continue\n\n    const path = basePath ? `${basePath}.${key}` : key\n    const currentValue = s.get(path)\n\n    // Primitives or arrays: set only if truly missing/undefined\n    if (!isObj(defaultValue)) {\n      if (currentValue === undefined) s.set(path, defaultValue)\n      continue\n    }\n\n    // Objects:\n    if (currentValue === undefined || !isObj(currentValue)) {\n      // If missing or wrong shape, seed the whole object from defaults\n      s.set(path, defaultValue)\n    } else {\n      // Recurse to fill only missing leaves\n      ensureDefaultsDeep(s, defaultValue, path, exclude)\n    }\n  }\n}\n\n// Load cached values from SQLite and migrate from legacy electron-store if needed\nexport async function initializeStore() {\n  // 1) Load from SQLite KV for known top-level keys\n  const topLevelKeys: string[] = [\n    STORE_KEYS.MAIN,\n    STORE_KEYS.ONBOARDING,\n    STORE_KEYS.SETTINGS,\n    STORE_KEYS.AUTH,\n    STORE_KEYS.ADVANCED_SETTINGS,\n    STORE_KEYS.OPEN_MIC,\n    STORE_KEYS.SELECTED_AUDIO_INPUT,\n    STORE_KEYS.INTERACTION_SOUNDS,\n    STORE_KEYS.USER_PROFILE,\n    STORE_KEYS.ID_TOKEN,\n    STORE_KEYS.ACCESS_TOKEN,\n    'appliedMigrations',\n  ]\n\n  for (const key of topLevelKeys) {\n    try {\n      const str = await KeyValueStore.get(key)\n      if (str !== undefined) {\n        try {\n          cache[key] = JSON.parse(str)\n        } catch {\n          cache[key] = str\n        }\n      }\n    } catch (err) {\n      console.error('[store] Failed loading key from SQLite', key, err)\n    }\n  }\n\n  // 2) One-time migration from electron-store → SQLite (backwards compatibility)\n  try {\n    const migrated = await KeyValueStore.get('migration:electron_store_v1_done')\n    if (migrated !== 'true') {\n      try {\n        const { default: LegacyStore } = await import('electron-store')\n        const legacy = new LegacyStore<AppStore>({ defaults: defaultValues })\n        const migrateKeys = [\n          STORE_KEYS.MAIN,\n          STORE_KEYS.ONBOARDING,\n          STORE_KEYS.SETTINGS,\n          STORE_KEYS.AUTH,\n          STORE_KEYS.ADVANCED_SETTINGS,\n          STORE_KEYS.OPEN_MIC,\n          STORE_KEYS.SELECTED_AUDIO_INPUT,\n          STORE_KEYS.INTERACTION_SOUNDS,\n          STORE_KEYS.USER_PROFILE,\n          STORE_KEYS.ID_TOKEN,\n          STORE_KEYS.ACCESS_TOKEN,\n          'appliedMigrations',\n        ]\n        for (const key of migrateKeys) {\n          try {\n            const fromLegacy = legacy.get(key as any)\n            if (fromLegacy !== undefined) {\n              // If cache value equals default and legacy has a differing value, prefer legacy\n              cache[key] = fromLegacy\n              await KeyValueStore.set(key, JSON.stringify(fromLegacy))\n            }\n          } catch (err) {\n            console.warn('[store] Legacy migration read failed for', key, err)\n          }\n        }\n        await KeyValueStore.set('migration:electron_store_v1_done', 'true')\n      } catch (err) {\n        console.warn(\n          '[store] Legacy electron-store not available, skipping migration',\n          err,\n        )\n      }\n    }\n  } catch (err) {\n    console.error('[store] Failed checking migration marker', err)\n  }\n\n  // 3) Ensure defaults are present for any missing values\n  ensureDefaultsDeep(store, defaultValues)\n\n  // 4) Run migrations (idempotent) unless tests explicitly skip\n  if (process.env.NODE_ENV !== 'test') {\n    runMigrations(store, migrations)\n  }\n}\n\nexport default store\n"
  },
  {
    "path": "lib/main/syncService.test.ts",
    "content": "import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'\n\n// Mock external boundaries only - let internal logic run naturally\n\n// Mock gRPC client\nconst mockGrpcClient = {\n  createNote: mock(() => Promise.resolve()),\n  updateNote: mock(() => Promise.resolve()),\n  deleteNote: mock(() => Promise.resolve()),\n  listNotesSince: mock(() => Promise.resolve([] as any)),\n  createInteraction: mock(() => Promise.resolve()),\n  updateInteraction: mock(() => Promise.resolve()),\n  deleteInteraction: mock(() => Promise.resolve()),\n  listInteractionsSince: mock(() => Promise.resolve([] as any)),\n  createDictionaryItem: mock(() => Promise.resolve()),\n  updateDictionaryItem: mock(() => Promise.resolve()),\n  deleteDictionaryItem: mock(() => Promise.resolve()),\n  listDictionaryItemsSince: mock(() => Promise.resolve([] as any)),\n}\nmock.module('../clients/grpcClient', () => ({\n  grpcClient: mockGrpcClient,\n}))\n\n// Mock electron store\nconst mockMainStore = {\n  get: mock(),\n}\nmock.module('./store', () => ({\n  default: mockMainStore,\n  getCurrentUserId: mock(() => 'test-user-123'),\n  createNewAuthState: mock(() => ({\n    state: 'test-state',\n    codeVerifier: 'test-verifier',\n  })),\n}))\n\n// Mock repository classes and KeyValueStore\nconst mockNotesTable = {\n  findModifiedSince: mock(() => Promise.resolve([] as any)),\n  upsert: mock(() => Promise.resolve()),\n  softDelete: mock(() => Promise.resolve()),\n  insert: mock(() => Promise.resolve({ id: 'test-id' })),\n  findById: mock(() => Promise.resolve(undefined)),\n  findAll: mock(() => Promise.resolve([] as any)),\n  findByInteractionId: mock(() => Promise.resolve([] as any)),\n  updateContent: mock(() => Promise.resolve()),\n  deleteAllUserData: mock(() => Promise.resolve()),\n}\n\nconst mockInteractionsTable = {\n  findModifiedSince: mock(() => Promise.resolve([] as any)),\n  upsert: mock(() => Promise.resolve()),\n  softDelete: mock(() => Promise.resolve()),\n  insert: mock(() => Promise.resolve({ id: 'test-id' })),\n  findById: mock(() => Promise.resolve(undefined)),\n  findAll: mock(() => Promise.resolve([] as any)),\n  deleteAllUserData: mock(() => Promise.resolve()),\n}\n\nconst mockDictionaryTable = {\n  findModifiedSince: mock(() => Promise.resolve([] as any)),\n  upsert: mock(() => Promise.resolve()),\n  softDelete: mock(() => Promise.resolve()),\n  insert: mock(() => Promise.resolve({ id: 'test-id' })),\n  findById: mock(() => Promise.resolve(undefined)),\n  findAll: mock(() => Promise.resolve([] as any)),\n  update: mock(() => Promise.resolve()),\n  deleteAllUserData: mock(() => Promise.resolve()),\n}\n\nconst mockKeyValueStore = {\n  get: mock(() => Promise.resolve(undefined as any)),\n  set: mock(() => Promise.resolve()),\n}\n\nmock.module('./sqlite/repo', () => ({\n  NotesTable: mockNotesTable,\n  InteractionsTable: mockInteractionsTable,\n  DictionaryTable: mockDictionaryTable,\n  KeyValueStore: mockKeyValueStore,\n}))\n\n// Mock console to avoid noise\nbeforeEach(() => {\n  console.log = mock()\n  console.error = mock()\n  console.info = mock()\n})\n\nimport { syncService } from './syncService'\nimport { STORE_KEYS } from '../constants/store-keys'\n\n// Track sync service calls\nlet syncIntervalId: any = null\nconst mockSetInterval = mock((fn: () => void, _delay: number) => {\n  syncIntervalId = setTimeout(fn, 0) // Execute immediately for testing\n  return syncIntervalId\n})\nconst mockClearInterval = mock((id: any) => {\n  if (id && typeof id === 'number') {\n    clearTimeout(id)\n  }\n})\n\ndescribe('SyncService Integration Tests', () => {\n  beforeEach(() => {\n    // Replace timers with mocks that work properly\n    global.setInterval = mockSetInterval as any\n    global.clearInterval = mockClearInterval as any\n\n    // Reset all mocks\n    Object.values(mockGrpcClient).forEach(mock => mock.mockClear())\n    mockMainStore.get.mockClear()\n\n    // Reset repository mocks\n    Object.values(mockNotesTable).forEach(mock => mock.mockClear())\n    Object.values(mockInteractionsTable).forEach(mock => mock.mockClear())\n    Object.values(mockDictionaryTable).forEach(mock => mock.mockClear())\n    Object.values(mockKeyValueStore).forEach(mock => mock.mockClear())\n\n    mockSetInterval.mockClear()\n    mockClearInterval.mockClear()\n\n    // Setup default user profile\n    mockMainStore.get.mockImplementation((key: string) => {\n      if (key === STORE_KEYS.USER_PROFILE) {\n        return { id: 'test-user-123' }\n      }\n      return null\n    })\n\n    // Setup default last sync time (KeyValueStore.get)\n    mockKeyValueStore.get.mockResolvedValue('2024-01-01T00:00:00.000Z')\n\n    // Reset repository methods to return empty by default\n    mockNotesTable.findModifiedSince.mockResolvedValue([])\n    mockInteractionsTable.findModifiedSince.mockResolvedValue([])\n    mockDictionaryTable.findModifiedSince.mockResolvedValue([])\n  })\n\n  afterEach(() => {\n    // Stop any running sync to clean up\n    syncService.stop()\n\n    // Clear any pending timeouts\n    if (syncIntervalId) {\n      clearTimeout(syncIntervalId)\n      syncIntervalId = null\n    }\n  })\n\n  describe('Sync Service Lifecycle', () => {\n    test('should skip sync when no user is logged in', async () => {\n      mockMainStore.get.mockReturnValue(null) // No user profile\n\n      await syncService.start()\n\n      // Should not attempt any gRPC operations\n      Object.values(mockGrpcClient).forEach(mockFn => {\n        expect(mockFn).not.toHaveBeenCalled()\n      })\n    })\n\n    test('should skip sync when user profile is missing ID', async () => {\n      mockMainStore.get.mockReturnValue({ name: 'Test User' }) // Missing ID\n\n      await syncService.start()\n\n      // Should not attempt any gRPC operations\n      Object.values(mockGrpcClient).forEach(mockFn => {\n        expect(mockFn).not.toHaveBeenCalled()\n      })\n    })\n  })\n\n  describe('Push Operations', () => {\n    test('should push new notes to server', async () => {\n      const testNote = {\n        id: 'note-123',\n        user_id: 'test-user-123',\n        content: 'Test note content',\n        created_at: '2024-01-02T00:00:00.000Z', // After last sync\n        updated_at: '2024-01-02T00:00:00.000Z',\n        deleted_at: null,\n        interaction_id: null,\n      }\n\n      // Mock modified notes query\n      mockNotesTable.findModifiedSince.mockResolvedValueOnce([testNote])\n\n      await syncService.start()\n\n      expect(mockGrpcClient.createNote).toHaveBeenCalledWith(testNote)\n    })\n\n    test('should push updated notes to server', async () => {\n      const testNote = {\n        id: 'note-123',\n        user_id: 'test-user-123',\n        content: 'Updated note content',\n        created_at: '2023-12-01T00:00:00.000Z', // Before last sync\n        updated_at: '2024-01-02T00:00:00.000Z', // After last sync\n        deleted_at: null,\n        interaction_id: null,\n      }\n\n      mockNotesTable.findModifiedSince.mockResolvedValueOnce([testNote])\n\n      await syncService.start()\n\n      expect(mockGrpcClient.updateNote).toHaveBeenCalledWith(testNote)\n    })\n\n    test('should push deleted notes to server', async () => {\n      const deletedNote = {\n        id: 'note-123',\n        user_id: 'test-user-123',\n        content: 'Deleted note',\n        created_at: '2023-12-01T00:00:00.000Z',\n        updated_at: '2024-01-02T00:00:00.000Z',\n        deleted_at: '2024-01-02T00:00:00.000Z',\n        interaction_id: null,\n      }\n\n      mockNotesTable.findModifiedSince.mockResolvedValueOnce([deletedNote])\n\n      await syncService.start()\n\n      expect(mockGrpcClient.deleteNote).toHaveBeenCalledWith(deletedNote)\n    })\n\n    test('should handle push errors gracefully', async () => {\n      const testNote = {\n        id: 'note-123',\n        user_id: 'test-user-123',\n        content: 'Test note',\n        created_at: '2024-01-02T00:00:00.000Z',\n        updated_at: '2024-01-02T00:00:00.000Z',\n        deleted_at: null,\n        interaction_id: null,\n      }\n\n      mockNotesTable.findModifiedSince.mockResolvedValueOnce([testNote])\n      mockGrpcClient.createNote.mockRejectedValueOnce(\n        new Error('Network error'),\n      )\n\n      await syncService.start()\n\n      // Should continue sync despite error (error is logged)\n      expect(mockGrpcClient.createNote).toHaveBeenCalledWith(testNote)\n      // Should still attempt to pull data\n      expect(mockGrpcClient.listNotesSince).toHaveBeenCalled()\n    })\n  })\n\n  describe('Pull Operations', () => {\n    test('should pull and upsert remote notes', async () => {\n      const remoteNote = {\n        id: 'remote-note-123',\n        userId: 'test-user-123',\n        content: 'Remote note content',\n        createdAt: '2024-01-02T00:00:00.000Z',\n        updatedAt: '2024-01-02T00:00:00.000Z',\n        deletedAt: null,\n        interactionId: null,\n      }\n\n      mockGrpcClient.listNotesSince.mockResolvedValueOnce([remoteNote])\n\n      await syncService.start()\n\n      // Should call database to upsert the remote note\n      expect(mockNotesTable.upsert).toHaveBeenCalled()\n    })\n\n    test('should handle remote note deletions', async () => {\n      const deletedRemoteNote = {\n        id: 'remote-note-123',\n        userId: 'test-user-123',\n        content: 'Deleted remote note',\n        createdAt: '2024-01-01T00:00:00.000Z',\n        updatedAt: '2024-01-02T00:00:00.000Z',\n        deletedAt: '2024-01-02T00:00:00.000Z',\n        interactionId: null,\n      }\n\n      mockGrpcClient.listNotesSince.mockResolvedValueOnce([deletedRemoteNote])\n\n      await syncService.start()\n\n      // Should call database to soft delete the note\n      expect(mockNotesTable.softDelete).toHaveBeenCalledWith(\n        deletedRemoteNote.id,\n      )\n    })\n\n    test('should handle interactions with raw audio data', async () => {\n      const audioData = new Uint8Array([1, 2, 3, 4, 5])\n      const remoteInteraction = {\n        id: 'remote-interaction-123',\n        userId: 'test-user-123',\n        title: 'Remote interaction',\n        asrOutput: JSON.stringify({ transcript: 'Hello world' }),\n        llmOutput: JSON.stringify({ response: 'Hi there' }),\n        rawAudio: audioData,\n        durationMs: 1500,\n        createdAt: '2024-01-02T00:00:00.000Z',\n        updatedAt: '2024-01-02T00:00:00.000Z',\n        deletedAt: null,\n      }\n\n      mockGrpcClient.listInteractionsSince.mockResolvedValueOnce([\n        remoteInteraction,\n      ])\n\n      await syncService.start()\n\n      // Should handle audio buffer conversion and database upsert\n      expect(mockInteractionsTable.upsert).toHaveBeenCalled()\n    })\n\n    test('should handle dictionary items correctly', async () => {\n      const remoteDictionaryItem = {\n        id: 'remote-dict-123',\n        userId: 'test-user-123',\n        word: 'pronunciation',\n        pronunciation: '/prəˌnʌnsiˈeɪʃən/',\n        createdAt: '2024-01-02T00:00:00.000Z',\n        updatedAt: '2024-01-02T00:00:00.000Z',\n        deletedAt: null,\n      }\n\n      mockGrpcClient.listDictionaryItemsSince.mockResolvedValueOnce([\n        remoteDictionaryItem,\n      ])\n\n      await syncService.start()\n\n      // Should upsert dictionary item\n      expect(mockDictionaryTable.upsert).toHaveBeenCalled()\n    })\n  })\n\n  describe('Sync Timing and State', () => {\n    test('should handle first-time sync (no previous sync timestamp)', async () => {\n      mockKeyValueStore.get.mockResolvedValueOnce(undefined) // No previous sync\n\n      await syncService.start()\n      const epoch = new Date(0).toISOString()\n\n      // Should still call pull operations (with undefined lastSyncedAt)\n      expect(mockGrpcClient.listNotesSince).toHaveBeenCalledWith(epoch)\n      expect(mockGrpcClient.listInteractionsSince).toHaveBeenCalledWith(epoch)\n      expect(mockGrpcClient.listDictionaryItemsSince).toHaveBeenCalledWith(\n        epoch,\n      )\n    })\n\n    test('should handle sync errors and continue operation', async () => {\n      mockNotesTable.findModifiedSince.mockRejectedValueOnce(\n        new Error('Database error'),\n      )\n      // Other operations should still succeed\n      mockGrpcClient.listNotesSince.mockResolvedValue([])\n\n      await syncService.start()\n\n      // When push operations fail, the entire sync cycle fails\n      // and no pull operations are attempted\n      expect(mockGrpcClient.listNotesSince).not.toHaveBeenCalled()\n      expect(mockGrpcClient.listInteractionsSince).not.toHaveBeenCalled()\n      expect(mockGrpcClient.listDictionaryItemsSince).not.toHaveBeenCalled()\n    })\n  })\n\n  describe('Data Type Integration', () => {\n    test('should handle mixed operations (create, update, delete)', async () => {\n      const newNote = {\n        id: 'new-note',\n        created_at: '2024-01-02T00:00:00.000Z', // After last sync\n        updated_at: '2024-01-02T00:00:00.000Z',\n        deleted_at: null,\n      }\n\n      const updatedNote = {\n        id: 'updated-note',\n        created_at: '2023-12-01T00:00:00.000Z', // Before last sync\n        updated_at: '2024-01-02T00:00:00.000Z', // After last sync\n        deleted_at: null,\n      }\n\n      const deletedNote = {\n        id: 'deleted-note',\n        created_at: '2023-12-01T00:00:00.000Z',\n        updated_at: '2024-01-02T00:00:00.000Z',\n        deleted_at: '2024-01-02T00:00:00.000Z',\n      }\n\n      mockNotesTable.findModifiedSince.mockResolvedValueOnce([\n        newNote,\n        updatedNote,\n        deletedNote,\n      ])\n\n      await syncService.start()\n\n      // Verify correct operations based on timestamps and deletion status\n      expect(mockGrpcClient.createNote).toHaveBeenCalledWith(newNote)\n      expect(mockGrpcClient.updateNote).toHaveBeenCalledWith(updatedNote)\n      expect(mockGrpcClient.deleteNote).toHaveBeenCalledWith(deletedNote)\n    })\n  })\n\n  describe('Critical Business Logic', () => {\n    test('first sync', async () => {\n      // Clear the default mock and set to return undefined for all calls\n      mockKeyValueStore.get.mockReset()\n      mockKeyValueStore.get.mockResolvedValue(undefined) // No previous sync\n      const epoch = new Date(0).toISOString()\n\n      await syncService.start()\n\n      // Should NOT push anything (no lastSyncedAt means first sync)\n      expect(mockNotesTable.findModifiedSince).toHaveBeenCalledWith(epoch)\n      expect(mockInteractionsTable.findModifiedSince).toHaveBeenCalledWith(\n        epoch,\n      )\n      expect(mockDictionaryTable.findModifiedSince).toHaveBeenCalledWith(epoch)\n\n      // Should still pull everything\n      expect(mockGrpcClient.listNotesSince).toHaveBeenCalledWith(epoch)\n      expect(mockGrpcClient.listInteractionsSince).toHaveBeenCalledWith(epoch)\n      expect(mockGrpcClient.listDictionaryItemsSince).toHaveBeenCalledWith(\n        epoch,\n      )\n    })\n\n    test('should push and pull data on subsequent syncs', async () => {\n      mockKeyValueStore.get.mockResolvedValueOnce('2024-01-01T00:00:00.000Z') // Has previous sync\n\n      await syncService.start()\n\n      // Should push (checking for modifications since last sync)\n      expect(mockNotesTable.findModifiedSince).toHaveBeenCalledWith(\n        '2024-01-01T00:00:00.000Z',\n      )\n      expect(mockInteractionsTable.findModifiedSince).toHaveBeenCalledWith(\n        '2024-01-01T00:00:00.000Z',\n      )\n      expect(mockDictionaryTable.findModifiedSince).toHaveBeenCalledWith(\n        '2024-01-01T00:00:00.000Z',\n      )\n\n      // Should also pull\n      expect(mockGrpcClient.listNotesSince).toHaveBeenCalledWith(\n        '2024-01-01T00:00:00.000Z',\n      )\n      expect(mockGrpcClient.listInteractionsSince).toHaveBeenCalledWith(\n        '2024-01-01T00:00:00.000Z',\n      )\n      expect(mockGrpcClient.listDictionaryItemsSince).toHaveBeenCalledWith(\n        '2024-01-01T00:00:00.000Z',\n      )\n    })\n\n    test('should prevent concurrent sync operations', async () => {\n      // Make sync take longer by delaying gRPC calls\n      let callCount = 0\n      mockGrpcClient.listNotesSince.mockImplementation(() => {\n        callCount++\n        return new Promise(resolve => setTimeout(() => resolve([]), 30))\n      })\n\n      // Start first sync\n      const syncPromise1 = syncService.start()\n\n      // Small delay to ensure first sync starts\n      await new Promise(resolve => setTimeout(resolve, 10))\n\n      // Start second sync (should be ignored due to isSyncing flag)\n      const syncPromise2 = syncService.start()\n\n      await Promise.all([syncPromise1, syncPromise2])\n\n      // Should only call gRPC operations once (second sync ignored)\n      expect(callCount).toBe(1)\n    })\n\n    test('should continue processing other items when individual items fail', async () => {\n      const note1 = { id: 'note-1', created_at: '2024-01-02T00:00:00.000Z' }\n      const note2 = { id: 'note-2', created_at: '2024-01-02T00:00:00.000Z' }\n      const note3 = { id: 'note-3', created_at: '2024-01-02T00:00:00.000Z' }\n\n      mockNotesTable.findModifiedSince.mockResolvedValueOnce([\n        note1,\n        note2,\n        note3,\n      ])\n\n      // Make second note fail\n      mockGrpcClient.createNote\n        .mockResolvedValueOnce(undefined) // note1 succeeds\n        .mockRejectedValueOnce(new Error('Network error')) // note2 fails\n        .mockResolvedValueOnce(undefined) // note3 should still be processed\n\n      await syncService.start()\n\n      // All three notes should be attempted\n      expect(mockGrpcClient.createNote).toHaveBeenCalledTimes(3)\n      expect(mockGrpcClient.createNote).toHaveBeenCalledWith(note1)\n      expect(mockGrpcClient.createNote).toHaveBeenCalledWith(note2)\n      expect(mockGrpcClient.createNote).toHaveBeenCalledWith(note3)\n\n      // Sync should continue to pull operations despite push failure\n      expect(mockGrpcClient.listNotesSince).toHaveBeenCalled()\n    })\n\n    test('should handle malformed JSON in interaction data gracefully', async () => {\n      const malformedInteraction = {\n        id: 'malformed-interaction',\n        userId: 'test-user-123',\n        asrOutput: 'invalid-json{', // Malformed JSON\n        llmOutput: '{\"valid\": true}',\n        rawAudio: null,\n        createdAt: '2024-01-02T00:00:00.000Z',\n        updatedAt: '2024-01-02T00:00:00.000Z',\n        deletedAt: null,\n      }\n\n      mockGrpcClient.listInteractionsSince.mockResolvedValueOnce([\n        malformedInteraction,\n      ])\n\n      // Should not crash on malformed JSON\n      await syncService.start()\n\n      // Should continue processing despite JSON error (error logged)\n      expect(mockGrpcClient.listNotesSince).toHaveBeenCalled()\n    })\n\n    test('should handle complex raw audio buffer conversion', async () => {\n      const audioData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])\n      const interactionWithAudio = {\n        id: 'audio-interaction',\n        userId: 'test-user-123',\n        title: 'Audio test',\n        asrOutput: JSON.stringify({ transcript: 'Hello' }),\n        llmOutput: null,\n        rawAudio: audioData,\n        durationMs: 1500,\n        createdAt: '2024-01-02T00:00:00.000Z',\n        updatedAt: '2024-01-02T00:00:00.000Z',\n        deletedAt: null,\n      }\n\n      mockGrpcClient.listInteractionsSince.mockResolvedValueOnce([\n        interactionWithAudio,\n      ])\n\n      await syncService.start()\n\n      // Should convert Uint8Array to Buffer and upsert\n      expect(mockInteractionsTable.upsert).toHaveBeenCalledWith(\n        expect.objectContaining({\n          id: 'audio-interaction',\n          raw_audio: expect.any(Buffer),\n        }),\n      )\n    })\n\n    test('should handle empty raw audio data correctly', async () => {\n      const interactionWithEmptyAudio = {\n        id: 'empty-audio-interaction',\n        userId: 'test-user-123',\n        title: 'Empty audio test',\n        asrOutput: JSON.stringify({ transcript: 'No audio' }),\n        llmOutput: null,\n        rawAudio: new Uint8Array([]), // Empty audio\n        durationMs: 0,\n        createdAt: '2024-01-02T00:00:00.000Z',\n        updatedAt: '2024-01-02T00:00:00.000Z',\n        deletedAt: null,\n      }\n\n      mockGrpcClient.listInteractionsSince.mockResolvedValueOnce([\n        interactionWithEmptyAudio,\n      ])\n\n      await syncService.start()\n\n      // Should handle empty audio buffer gracefully\n      expect(mockInteractionsTable.upsert).toHaveBeenCalledWith(\n        expect.objectContaining({\n          id: 'empty-audio-interaction',\n          raw_audio: null, // Empty audio should become null\n        }),\n      )\n    })\n\n    test('should update timestamp only after successful complete sync', async () => {\n      // All operations succeed and at least one change is processed (ensures cursor advances)\n      const remoteNote = {\n        id: 'remote-note-for-timestamp',\n        userId: 'test-user-123',\n        content: 'Remote note content',\n        createdAt: '2024-01-02T00:00:00.000Z',\n        updatedAt: '2024-01-02T00:00:00.000Z',\n        deletedAt: null,\n        interactionId: null,\n      }\n      mockGrpcClient.listNotesSince.mockResolvedValueOnce([remoteNote])\n      mockGrpcClient.listInteractionsSince.mockResolvedValue([])\n      mockGrpcClient.listDictionaryItemsSince.mockResolvedValue([])\n\n      const startTime = Date.now()\n      await syncService.start()\n      const endTime = Date.now()\n\n      // Timestamp should be updated with current time using namespaced key\n      expect(mockKeyValueStore.set).toHaveBeenCalledTimes(1)\n      const [keyArg, valueArg] = mockKeyValueStore.set.mock.calls[0] as any\n      expect(typeof keyArg).toBe('string')\n      expect(keyArg.startsWith('lastSyncedAt:')).toBe(true)\n      expect(keyArg.endsWith(':test-user-123')).toBe(true)\n      expect(typeof valueArg).toBe('string')\n\n      // Verify timestamp is recent (within test execution window)\n      const timestamp = new Date(valueArg).getTime()\n      expect(timestamp).toBeGreaterThanOrEqual(startTime)\n      expect(timestamp).toBeLessThanOrEqual(endTime)\n    })\n  })\n\n  describe('Singleton Pattern Business Logic', () => {\n    test('should return same instance across multiple getInstance calls', async () => {\n      const { SyncService } = await import('./syncService')\n\n      const instance1 = SyncService.getInstance()\n      const instance2 = SyncService.getInstance()\n      const instance3 = SyncService.getInstance()\n\n      // All calls should return the exact same instance\n      expect(instance1).toBe(instance2)\n      expect(instance2).toBe(instance3)\n      expect(instance1).toBe(instance3)\n    })\n\n    test('should maintain singleton across different import patterns', async () => {\n      const { SyncService } = await import('./syncService')\n      const { syncService: exportedInstance } = await import('./syncService')\n\n      const getInstance = SyncService.getInstance()\n\n      // Exported instance should be same as getInstance\n      expect(exportedInstance).toBe(getInstance)\n    })\n  })\n})\n"
  },
  {
    "path": "lib/main/syncService.ts",
    "content": "import {\n  DictionaryTable,\n  InteractionsTable,\n  KeyValueStore,\n  NotesTable,\n} from './sqlite/repo'\nimport { grpcClient } from '../clients/grpcClient'\nimport { Note, Interaction, DictionaryItem } from './sqlite/models'\nimport mainStore from './store'\nimport { STORE_KEYS } from '../constants/store-keys'\nimport type { AdvancedSettings } from './store'\nimport { DEFAULT_ADVANCED_SETTINGS } from '../constants/generated-defaults.js'\nimport { main } from 'bun'\nimport { mainWindow } from './app'\n\nconst LAST_SYNCED_AT_KEY = 'lastSyncedAt'\n\nfunction getEnvNamespace(): string {\n  const baseUrl = (import.meta.env?.VITE_GRPC_BASE_URL as string) || ''\n  try {\n    const url = new URL(baseUrl)\n    return url.host || baseUrl || 'unknown'\n  } catch {\n    return baseUrl || 'unknown'\n  }\n}\n\nfunction getLastSyncedAtKey(userId: string): string {\n  const envNs = getEnvNamespace()\n  return `${LAST_SYNCED_AT_KEY}:${envNs}:${userId}`\n}\n\nexport class SyncService {\n  private isSyncing = false\n  private syncInterval: NodeJS.Timeout | null = null\n  private static instance: SyncService\n\n  private constructor() {\n    // Private constructor to ensure singleton pattern\n  }\n\n  public static getInstance(): SyncService {\n    if (!SyncService.instance) {\n      SyncService.instance = new SyncService()\n    }\n    return SyncService.instance\n  }\n\n  public async start() {\n    // Clear any existing interval\n    if (this.syncInterval) {\n      clearInterval(this.syncInterval)\n    }\n\n    // Initial sync on startup, then schedule periodic syncs\n    await this.runSync()\n    this.syncInterval = setInterval(() => this.runSync(), 1000 * 5) // Sync every 5 seconds\n  }\n\n  public stop() {\n    if (this.syncInterval) {\n      clearInterval(this.syncInterval)\n      this.syncInterval = null\n    }\n    this.isSyncing = false\n  }\n\n  private async runSync() {\n    if (this.isSyncing) {\n      return\n    }\n\n    this.isSyncing = true\n\n    try {\n      const user = mainStore.get(STORE_KEYS.USER_PROFILE) as any\n      if (!user?.id) {\n        console.log(\n          'No user logged in or user profile is missing ID. Skipping sync.',\n        )\n        this.isSyncing = false\n        return\n      }\n\n      const lastSyncedAtKey = getLastSyncedAtKey(user.id)\n      const lastSyncedAt =\n        (await KeyValueStore.get(lastSyncedAtKey)) || new Date(0).toISOString()\n\n      // =================================================================\n      // PUSH LOCAL CHANGES\n      // =================================================================\n      let processedChanges = 0\n      processedChanges += await this.pushNotes(lastSyncedAt)\n      processedChanges += await this.pushInteractions(lastSyncedAt)\n      processedChanges += await this.pushDictionaryItems(lastSyncedAt)\n\n      // =================================================================\n      // PULL REMOTE CHANGES\n      // =================================================================\n      processedChanges += await this.pullNotes(lastSyncedAt)\n      processedChanges += await this.pullInteractions(lastSyncedAt)\n      processedChanges += await this.pullDictionaryItems(lastSyncedAt)\n\n      // =================================================================\n      // SYNC ADVANCED SETTINGS\n      // =================================================================\n      await this.syncAdvancedSettings(lastSyncedAt)\n\n      if (processedChanges > 0) {\n        const newSyncTimestamp = new Date().toISOString()\n        await KeyValueStore.set(lastSyncedAtKey, newSyncTimestamp)\n      }\n    } catch (error) {\n      console.error('Sync cycle failed:', error)\n    } finally {\n      this.isSyncing = false\n    }\n  }\n\n  private async pushNotes(lastSyncedAt: string): Promise<number> {\n    const modifiedNotes = await NotesTable.findModifiedSince(lastSyncedAt)\n    if (modifiedNotes.length > 0) {\n      for (const note of modifiedNotes) {\n        try {\n          // If created_at is after lastSyncedAt, it's a new note\n          if (new Date(note.created_at) > new Date(lastSyncedAt)) {\n            await grpcClient.createNote(note)\n          } else if (note.deleted_at) {\n            await grpcClient.deleteNote(note)\n          } else {\n            await grpcClient.updateNote(note)\n          }\n        } catch (e) {\n          console.error(`Failed to push note ${note.id}:`, e)\n        }\n      }\n    }\n    return modifiedNotes.length\n  }\n\n  private async pushInteractions(lastSyncedAt: string): Promise<number> {\n    const modifiedInteractions =\n      await InteractionsTable.findModifiedSince(lastSyncedAt)\n    if (modifiedInteractions.length > 0) {\n      for (const interaction of modifiedInteractions) {\n        try {\n          if (new Date(interaction.created_at) > new Date(lastSyncedAt)) {\n            await grpcClient.createInteraction(interaction)\n          } else if (interaction.deleted_at) {\n            await grpcClient.deleteInteraction(interaction)\n          } else {\n            await grpcClient.updateInteraction(interaction)\n          }\n        } catch (e) {\n          console.error(`Failed to push interaction ${interaction.id}:`, e)\n        }\n      }\n    }\n    return modifiedInteractions.length\n  }\n\n  private async pushDictionaryItems(lastSyncedAt: string): Promise<number> {\n    const modifiedItems = await DictionaryTable.findModifiedSince(lastSyncedAt)\n    if (modifiedItems.length > 0) {\n      for (const item of modifiedItems) {\n        try {\n          if (new Date(item.created_at) > new Date(lastSyncedAt)) {\n            await grpcClient.createDictionaryItem(item)\n          } else if (item.deleted_at) {\n            await grpcClient.deleteDictionaryItem(item)\n          } else {\n            await grpcClient.updateDictionaryItem(item)\n          }\n        } catch (e) {\n          console.error(`Failed to push dictionary item ${item.id}:`, e)\n        }\n      }\n    }\n    return modifiedItems.length\n  }\n\n  private async pullNotes(lastSyncedAt?: string): Promise<number> {\n    const remoteNotes = await grpcClient.listNotesSince(lastSyncedAt)\n    if (remoteNotes.length > 0) {\n      for (const remoteNote of remoteNotes) {\n        if (remoteNote.deletedAt) {\n          await NotesTable.softDelete(remoteNote.id)\n          continue\n        }\n        const localNote: Note = {\n          id: remoteNote.id,\n          user_id: remoteNote.userId,\n          interaction_id: remoteNote.interactionId || null,\n          content: remoteNote.content,\n          created_at: remoteNote.createdAt,\n          updated_at: remoteNote.updatedAt,\n          deleted_at: remoteNote.deletedAt || null,\n        }\n        await NotesTable.upsert(localNote)\n      }\n    }\n    return remoteNotes.length\n  }\n\n  private async pullInteractions(lastSyncedAt?: string): Promise<number> {\n    const remoteInteractions =\n      await grpcClient.listInteractionsSince(lastSyncedAt)\n    if (remoteInteractions.length > 0) {\n      for (const remoteInteraction of remoteInteractions) {\n        if (remoteInteraction.deletedAt) {\n          await InteractionsTable.softDelete(remoteInteraction.id)\n          continue\n        }\n\n        // Convert Uint8Array back to Buffer\n        let audioBuffer: Buffer | null = null\n        if (\n          remoteInteraction.rawAudio &&\n          remoteInteraction.rawAudio.length > 0\n        ) {\n          audioBuffer = Buffer.from(\n            remoteInteraction.rawAudio.buffer,\n            remoteInteraction.rawAudio.byteOffset,\n            remoteInteraction.rawAudio.byteLength,\n          )\n        }\n\n        const localInteraction: Interaction = {\n          id: remoteInteraction.id,\n          user_id: remoteInteraction.userId || null,\n          title: remoteInteraction.title || null,\n          asr_output: remoteInteraction.asrOutput\n            ? JSON.parse(remoteInteraction.asrOutput)\n            : null,\n          llm_output: remoteInteraction.llmOutput\n            ? JSON.parse(remoteInteraction.llmOutput)\n            : null,\n          raw_audio: audioBuffer,\n          duration_ms: remoteInteraction.durationMs || 0,\n          created_at: remoteInteraction.createdAt,\n          updated_at: remoteInteraction.updatedAt,\n          deleted_at: remoteInteraction.deletedAt || null,\n          raw_audio_id: remoteInteraction.rawAudioId,\n          sample_rate: null,\n        }\n        await InteractionsTable.upsert(localInteraction)\n      }\n    }\n    return remoteInteractions.length\n  }\n\n  private async pullDictionaryItems(lastSyncedAt?: string): Promise<number> {\n    const remoteItems = await grpcClient.listDictionaryItemsSince(lastSyncedAt)\n    if (remoteItems.length > 0) {\n      for (const remoteItem of remoteItems) {\n        if (remoteItem.deletedAt) {\n          await DictionaryTable.softDelete(remoteItem.id)\n          continue\n        }\n        const localItem: DictionaryItem = {\n          id: remoteItem.id,\n          user_id: remoteItem.userId,\n          word: remoteItem.word,\n          pronunciation: remoteItem.pronunciation || null,\n          created_at: remoteItem.createdAt,\n          updated_at: remoteItem.updatedAt,\n          deleted_at: remoteItem.deletedAt || null,\n        }\n        await DictionaryTable.upsert(localItem)\n      }\n    }\n    return remoteItems.length\n  }\n\n  private async syncAdvancedSettings(lastSyncedAt?: string) {\n    try {\n      // Get remote advanced settings\n      const remoteSettings = await grpcClient.getAdvancedSettings()\n      if (!remoteSettings) {\n        console.warn('No remote advanced settings found, skipping sync.')\n        return\n      }\n\n      // Always update local defaults\n      const defaultSettings = remoteSettings.default\n      if (defaultSettings) {\n        const currentLocalSettings = mainStore.get(\n          STORE_KEYS.ADVANCED_SETTINGS,\n        ) as AdvancedSettings\n        mainStore.set(STORE_KEYS.ADVANCED_SETTINGS, {\n          ...currentLocalSettings,\n          defaults: defaultSettings,\n        })\n\n        // Notify UI of the update\n        if (\n          mainWindow &&\n          !mainWindow.isDestroyed() &&\n          !mainWindow.webContents.isDestroyed()\n        ) {\n          mainWindow.webContents.send('advanced-settings-updated')\n        }\n      }\n\n      // Compare timestamps to determine sync direction\n      const remoteUpdatedAt = new Date(remoteSettings.updatedAt)\n      const lastSyncTime = lastSyncedAt ? new Date(lastSyncedAt) : new Date(0)\n\n      // If remote settings were updated after last sync, pull them to local\n      if (remoteUpdatedAt > lastSyncTime) {\n        // Get current local settings to preserve local-only fields\n        const currentLocalSettings = mainStore.get(\n          STORE_KEYS.ADVANCED_SETTINGS,\n        ) as AdvancedSettings\n\n        const updatedLocalSettings: AdvancedSettings = {\n          llm: {\n            asrProvider: remoteSettings.llm?.asrProvider ?? null,\n            asrModel: remoteSettings.llm?.asrModel ?? null,\n            asrPrompt: remoteSettings.llm?.asrPrompt ?? null,\n            llmProvider: remoteSettings.llm?.llmProvider ?? null,\n            llmModel: remoteSettings.llm?.llmModel ?? null,\n            llmTemperature: remoteSettings.llm?.llmTemperature ?? null,\n            transcriptionPrompt:\n              remoteSettings.llm?.transcriptionPrompt ?? null,\n            editingPrompt: remoteSettings.llm?.editingPrompt ?? null,\n            noSpeechThreshold: remoteSettings.llm?.noSpeechThreshold ?? null,\n          },\n          // Preserve local-only settings that aren't synced to the server\n          grammarServiceEnabled:\n            currentLocalSettings?.grammarServiceEnabled ?? false,\n          // Preserve defaults that were set earlier in this function\n          defaults: currentLocalSettings?.defaults,\n          macosAccessibilityContextEnabled:\n            currentLocalSettings.macosAccessibilityContextEnabled ?? false,\n        }\n\n        // Update local store\n        mainStore.set(STORE_KEYS.ADVANCED_SETTINGS, updatedLocalSettings)\n        // Notify UI of the update\n        if (\n          mainWindow &&\n          !mainWindow.isDestroyed() &&\n          !mainWindow.webContents.isDestroyed()\n        ) {\n          mainWindow.webContents.send('advanced-settings-updated')\n        }\n      }\n      // Note: We don't push local changes to server in this implementation\n      // since advanced settings are typically managed through the UI which\n      // directly calls the server API. This sync is primarily for pulling\n      // changes made on other devices or through other clients.\n    } catch (error) {\n      console.error('Failed to sync advanced settings:', error)\n    }\n  }\n}\n\nexport const syncService = SyncService.getInstance()\n"
  },
  {
    "path": "lib/main/teardown.ts",
    "content": "import { exec } from 'child_process'\nimport { audioRecorderService } from '../media/audio'\nimport { stopKeyListener } from '../media/keyboard'\nimport { selectedTextReaderService } from '../media/selected-text-reader'\nimport { allowAppNap } from './appNap'\nimport { syncService } from './syncService'\nimport { destroyAppTray } from './tray'\nimport { timingCollector } from './timing/TimingCollector'\n\nexport const teardown = () => {\n  stopKeyListener()\n  audioRecorderService.terminate()\n  selectedTextReaderService.terminate()\n  timingCollector.shutdown()\n  syncService.stop()\n  destroyAppTray()\n  allowAppNap()\n}\n\nconst WIN_HELPERS = [\n  'global-key-listener.exe',\n  'audio-recorder.exe',\n  'text-writer.exe',\n  'active-application.exe',\n  'selected-text-reader.exe',\n  'electron-crashpad-handler.exe',\n]\n\nconst MAC_HELPERS = [\n  'global-key-listener',\n  'audio-recorder',\n  'text-writer',\n  'active-application',\n  'selected-text-reader',\n  'electron-crashpad-handler',\n  // Electron’s helpers (your app name may differ)\n  'Ito Helper',\n  'Ito Helper (Renderer)',\n  'Ito Helper (GPU)',\n  'Ito Helper (Plugin)',\n]\n\nexport function killByName(name: string): Promise<void> {\n  return new Promise(resolve => {\n    const cmd =\n      process.platform === 'win32'\n        ? `taskkill /IM \"${name}\" /T /F`\n        : `pkill -f \"${name}\" || true`\n    exec(cmd, () => resolve())\n  })\n}\n\nexport async function hardKillAll(): Promise<void> {\n  const names = process.platform === 'win32' ? WIN_HELPERS : MAC_HELPERS\n  for (const n of names) {\n    try {\n      await killByName(n)\n    } catch {\n      /* empty */\n    }\n  }\n  // tiny grace window for handle release\n  await new Promise(r => setTimeout(r, 500))\n}\n"
  },
  {
    "path": "lib/main/text/TextInserter.test.ts",
    "content": "import { describe, test, expect, beforeEach, mock } from 'bun:test'\n\n// Mock the text-writer module\nconst mockSetFocusedText = mock(() => Promise.resolve(true))\nmock.module('../../media/text-writer', () => ({\n  setFocusedText: mockSetFocusedText,\n}))\n\nimport { TextInserter } from './TextInserter'\n\ndescribe('TextInserter', () => {\n  let textInserter: TextInserter\n\n  beforeEach(() => {\n    textInserter = new TextInserter()\n    mockSetFocusedText.mockClear()\n\n    // Reset default mock behavior\n    mockSetFocusedText.mockResolvedValue(true)\n  })\n\n  describe('Text Insertion', () => {\n    test('should insert text successfully', async () => {\n      const transcript = 'Hello world'\n      const result = await textInserter.insertText(transcript)\n\n      expect(result).toBe(true)\n      expect(mockSetFocusedText).toHaveBeenCalledWith(transcript)\n    })\n\n    test('should return false for empty transcript', async () => {\n      const result = await textInserter.insertText('')\n\n      expect(result).toBe(false)\n      expect(mockSetFocusedText).not.toHaveBeenCalled()\n    })\n\n    test('should return false for transcript of whitespace', async () => {\n      let result = await textInserter.insertText(' ')\n      expect(result).toBe(false)\n      expect(mockSetFocusedText).not.toHaveBeenCalled()\n\n      result = await textInserter.insertText('\\n')\n      expect(result).toBe(false)\n      expect(mockSetFocusedText).not.toHaveBeenCalled()\n    })\n\n    test('should return false for null transcript', async () => {\n      const result = await textInserter.insertText(null as any)\n\n      expect(result).toBe(false)\n      expect(mockSetFocusedText).not.toHaveBeenCalled()\n    })\n\n    test('should return false for undefined transcript', async () => {\n      const result = await textInserter.insertText(undefined as any)\n\n      expect(result).toBe(false)\n      expect(mockSetFocusedText).not.toHaveBeenCalled()\n    })\n\n    test('should handle different transcript types', async () => {\n      const transcripts = [\n        'Short text',\n        'This is a longer transcript with multiple words and punctuation.',\n        'Special characters: !@#$%^&*()',\n        'Numbers: 123 456 789',\n        'Mixed: Hello 123 World!',\n      ]\n\n      for (const transcript of transcripts) {\n        const result = await textInserter.insertText(transcript)\n        expect(result).toBe(true)\n        expect(mockSetFocusedText).toHaveBeenCalledWith(transcript)\n      }\n\n      expect(mockSetFocusedText).toHaveBeenCalledTimes(transcripts.length)\n    })\n  })\n\n  describe('Error Handling', () => {\n    test('should handle setFocusedText returning false', async () => {\n      mockSetFocusedText.mockResolvedValue(false)\n\n      const result = await textInserter.insertText('test')\n\n      expect(result).toBe(false)\n      expect(mockSetFocusedText).toHaveBeenCalledWith('test')\n    })\n\n    test('should handle setFocusedText throwing error', async () => {\n      const testError = new Error('Text insertion failed')\n      mockSetFocusedText.mockRejectedValue(testError)\n\n      const result = await textInserter.insertText('test')\n\n      expect(result).toBe(false)\n      expect(mockSetFocusedText).toHaveBeenCalledWith('test')\n    })\n\n    test('should handle setFocusedText throwing non-Error object', async () => {\n      mockSetFocusedText.mockRejectedValue('String error')\n\n      const result = await textInserter.insertText('test')\n\n      expect(result).toBe(false)\n    })\n  })\n\n  describe('Integration Scenarios', () => {\n    test('should handle multiple sequential insertions', async () => {\n      const transcripts = ['First', 'Second', 'Third']\n\n      for (const transcript of transcripts) {\n        const result = await textInserter.insertText(transcript)\n        expect(result).toBe(true)\n      }\n\n      expect(mockSetFocusedText).toHaveBeenCalledTimes(3)\n    })\n\n    test('should handle mixed success and failure scenarios', async () => {\n      // First call succeeds\n      mockSetFocusedText.mockResolvedValueOnce(true)\n      const result1 = await textInserter.insertText('Success')\n      expect(result1).toBe(true)\n\n      // Second call fails\n      mockSetFocusedText.mockResolvedValueOnce(false)\n      const result2 = await textInserter.insertText('Failure')\n      expect(result2).toBe(false)\n\n      // Third call throws error\n      mockSetFocusedText.mockRejectedValueOnce(new Error('Error case'))\n      const result3 = await textInserter.insertText('Error')\n      expect(result3).toBe(false)\n\n      expect(mockSetFocusedText).toHaveBeenCalledTimes(3)\n    })\n  })\n})\n"
  },
  {
    "path": "lib/main/text/TextInserter.ts",
    "content": "import { setFocusedText } from '../../media/text-writer'\nimport { timingCollector, TimingEventName } from '../timing/TimingCollector'\n\nexport class TextInserter {\n  async insertText(transcript: string): Promise<boolean> {\n    // If the string is empty, don't insert\n    if (!transcript || !transcript.trim()) {\n      return false\n    }\n\n    try {\n      return await timingCollector.timeAsync(\n        TimingEventName.TEXT_WRITER,\n        async () => await setFocusedText(transcript),\n      )\n    } catch (error) {\n      console.error('Error inserting text:', error)\n      return false\n    }\n  }\n}\n"
  },
  {
    "path": "lib/main/timing/TimingCollector.test.ts",
    "content": "import { describe, test, expect, beforeEach, mock, afterEach } from 'bun:test'\n\n// Mock electron-log\nmock.module('electron-log', () => ({\n  default: {\n    info: mock(),\n    warn: mock(),\n    error: mock(),\n  },\n}))\n\n// Mock store\nconst mockStore = {\n  get: mock((key: string) => {\n    if (key === 'settings') {\n      return { shareAnalytics: true }\n    }\n    return undefined\n  }),\n}\nmock.module('../store', () => ({\n  default: mockStore,\n  store: mockStore,\n  getCurrentUserId: mock(() => 'test-user-id'),\n}))\n\n// Mock grpcClient\nconst mockSubmitTimingReports = mock(() => Promise.resolve({}))\nmock.module('../../clients/grpcClient', () => ({\n  grpcClient: {\n    submitTimingReports: mockSubmitTimingReports,\n  },\n}))\n\n// Import after all mocks are set up\nimport { TimingCollector, TimingEventName } from './TimingCollector'\n\ndescribe('TimingCollector', () => {\n  let timingCollector: TimingCollector\n  let originalDateNow: typeof Date.now\n\n  beforeEach(() => {\n    // Capture original Date.now\n    originalDateNow = Date.now\n\n    // Create a fresh instance for each test\n    timingCollector = new TimingCollector()\n\n    // Clear all mocks\n    mockStore.get.mockClear()\n    mockSubmitTimingReports.mockClear()\n\n    // Reset default behaviors\n    mockStore.get.mockImplementation((key: string) => {\n      if (key === 'settings') {\n        return { shareAnalytics: true }\n      }\n      return undefined\n    })\n    mockSubmitTimingReports.mockResolvedValue({})\n  })\n\n  afterEach(() => {\n    // Restore Date.now\n    Date.now = originalDateNow\n  })\n\n  describe('Interaction Lifecycle', () => {\n    test('should start tracking an interaction', async () => {\n      const interactionId = 'test-interaction-1'\n      timingCollector.startInteraction(interactionId)\n      timingCollector.finalizeInteraction(interactionId)\n\n      await timingCollector.flush()\n\n      // Should have sent a report\n      expect(mockSubmitTimingReports).toHaveBeenCalledTimes(1)\n    })\n\n    test('should not track if analytics disabled', async () => {\n      mockStore.get.mockImplementation((key: string) => {\n        if (key === 'settings') {\n          return { shareAnalytics: false }\n        }\n        return undefined\n      })\n\n      const interactionId = 'test-interaction-1'\n      timingCollector.startInteraction(interactionId)\n      timingCollector.finalizeInteraction(interactionId)\n\n      await timingCollector.flush()\n\n      // Should not have sent any reports\n      expect(mockSubmitTimingReports).not.toHaveBeenCalled()\n    })\n\n    test('should clear interaction without finalizing', async () => {\n      const interactionId = 'test-interaction-1'\n      timingCollector.startInteraction(interactionId)\n      timingCollector.clearInteraction(interactionId)\n\n      await timingCollector.flush()\n\n      // Should not have sent any reports (cleared before finalizing)\n      expect(mockSubmitTimingReports).not.toHaveBeenCalled()\n    })\n  })\n\n  describe('Timing Events', () => {\n    test('should record start and end timing', async () => {\n      const interactionId = 'test-interaction-1'\n      timingCollector.startInteraction(interactionId)\n\n      timingCollector.startTiming(TimingEventName.TEXT_WRITER, interactionId)\n      timingCollector.endTiming(TimingEventName.TEXT_WRITER, interactionId)\n\n      timingCollector.finalizeInteraction(interactionId)\n\n      await timingCollector.flush()\n\n      // Should have sent a report\n      expect(mockSubmitTimingReports).toHaveBeenCalledTimes(1)\n    })\n\n    test('should handle null interaction ID gracefully', async () => {\n      timingCollector.startTiming(TimingEventName.TEXT_WRITER)\n      timingCollector.endTiming(TimingEventName.TEXT_WRITER)\n\n      await timingCollector.flush()\n\n      // Should not send any reports\n      expect(mockSubmitTimingReports).not.toHaveBeenCalled()\n    })\n\n    test('should warn when ending timing for unknown interaction', async () => {\n      timingCollector.endTiming(\n        TimingEventName.TEXT_WRITER,\n        'unknown-interaction',\n      )\n\n      await timingCollector.flush()\n\n      // Should not send any reports (no interaction was started)\n      expect(mockSubmitTimingReports).not.toHaveBeenCalled()\n    })\n\n    test('should warn when ending timing for unknown event', async () => {\n      const interactionId = 'test-interaction-1'\n      timingCollector.startInteraction(interactionId)\n\n      // End timing without starting it\n      timingCollector.endTiming(TimingEventName.TEXT_WRITER, interactionId)\n      timingCollector.finalizeInteraction(interactionId)\n\n      await timingCollector.flush()\n\n      // Should still send a report even though event wasn't started\n      expect(mockSubmitTimingReports).toHaveBeenCalledTimes(1)\n    })\n  })\n\n  describe('timeAsync Utility', () => {\n    test('should wrap async function and time it', async () => {\n      const interactionId = 'test-interaction-1'\n      timingCollector.startInteraction(interactionId)\n\n      const mockFn = mock(async () => {\n        await new Promise(resolve => setTimeout(resolve, 10))\n        return 'result'\n      })\n\n      const result = await timingCollector.timeAsync(\n        TimingEventName.TEXT_WRITER,\n        mockFn,\n        interactionId,\n      )\n\n      expect(result).toBe('result')\n      expect(mockFn).toHaveBeenCalledTimes(1)\n    })\n\n    test('should time even when function throws', async () => {\n      const interactionId = 'test-interaction-1'\n      timingCollector.startInteraction(interactionId)\n\n      const mockFn = mock(async () => {\n        throw new Error('Test error')\n      })\n\n      try {\n        await timingCollector.timeAsync(\n          TimingEventName.TEXT_WRITER,\n          mockFn,\n          interactionId,\n        )\n        expect(false).toBe(true) // Should not reach here\n      } catch (error: any) {\n        expect(error.message).toBe('Test error')\n      }\n\n      // Timing should still be recorded and sent\n      timingCollector.finalizeInteraction(interactionId)\n      await timingCollector.flush()\n\n      expect(mockSubmitTimingReports).toHaveBeenCalledTimes(1)\n    })\n\n    test('should handle synchronous functions', async () => {\n      const interactionId = 'test-interaction-1'\n      timingCollector.startInteraction(interactionId)\n\n      const mockFn = mock(() => 'sync-result')\n\n      const result = await timingCollector.timeAsync(\n        TimingEventName.TEXT_WRITER,\n        mockFn,\n        interactionId,\n      )\n\n      expect(result).toBe('sync-result')\n      expect(mockFn).toHaveBeenCalledTimes(1)\n    })\n\n    test('should handle null interaction ID', async () => {\n      const mockFn = mock(() => 'result')\n\n      const result = await timingCollector.timeAsync(\n        TimingEventName.TEXT_WRITER,\n        mockFn,\n        undefined,\n      )\n\n      expect(result).toBe('result')\n      expect(mockFn).toHaveBeenCalledTimes(1)\n    })\n  })\n\n  describe('Finalization', () => {\n    test('should finalize interaction and create report', async () => {\n      const interactionId = 'test-interaction-1'\n      timingCollector.startInteraction(interactionId)\n\n      timingCollector.startTiming(\n        TimingEventName.INTERACTION_ACTIVE,\n        interactionId,\n      )\n      timingCollector.endTiming(\n        TimingEventName.INTERACTION_ACTIVE,\n        interactionId,\n      )\n\n      timingCollector.finalizeInteraction(interactionId)\n\n      await timingCollector.flush()\n\n      // Should have sent a report\n      expect(mockSubmitTimingReports).toHaveBeenCalledTimes(1)\n    })\n\n    test('should calculate total duration correctly', async () => {\n      const interactionId = 'test-interaction-1'\n      timingCollector.startInteraction(interactionId)\n\n      // First event\n      timingCollector.startTiming(\n        TimingEventName.INTERACTION_ACTIVE,\n        interactionId,\n      )\n      timingCollector.endTiming(\n        TimingEventName.INTERACTION_ACTIVE,\n        interactionId,\n      )\n\n      // Second event\n      timingCollector.startTiming(TimingEventName.TEXT_WRITER, interactionId)\n      timingCollector.endTiming(TimingEventName.TEXT_WRITER, interactionId)\n\n      timingCollector.finalizeInteraction(interactionId)\n\n      await timingCollector.flush()\n\n      // Should have sent a report with both events\n      expect(mockSubmitTimingReports).toHaveBeenCalledTimes(1)\n      const calls = mockSubmitTimingReports.mock.calls as any[]\n      const reports = calls[0][0]\n      expect(reports).toHaveLength(1)\n      expect(reports[0].events).toHaveLength(2)\n    })\n\n    test('should not finalize if analytics disabled', async () => {\n      mockStore.get.mockImplementation((key: string) => {\n        if (key === 'settings') {\n          return { shareAnalytics: false }\n        }\n        return undefined\n      })\n\n      const interactionId = 'test-interaction-1'\n      timingCollector.startInteraction(interactionId)\n      timingCollector.finalizeInteraction(interactionId)\n\n      await timingCollector.flush()\n\n      // Should not send any reports\n      expect(mockSubmitTimingReports).not.toHaveBeenCalled()\n    })\n\n    test('should warn when finalizing unknown interaction', async () => {\n      timingCollector.finalizeInteraction('unknown-interaction')\n\n      await timingCollector.flush()\n\n      // Should not send any reports\n      expect(mockSubmitTimingReports).not.toHaveBeenCalled()\n    })\n  })\n\n  describe('Flushing', () => {\n    test('should not flush if no reports', async () => {\n      await timingCollector.flush()\n      expect(mockSubmitTimingReports).not.toHaveBeenCalled()\n    })\n\n    test('should flush reports to server', async () => {\n      const interactionId = 'test-interaction-1'\n      timingCollector.startInteraction(interactionId)\n      timingCollector.startTiming(\n        TimingEventName.INTERACTION_ACTIVE,\n        interactionId,\n      )\n      timingCollector.endTiming(\n        TimingEventName.INTERACTION_ACTIVE,\n        interactionId,\n      )\n      timingCollector.finalizeInteraction(interactionId)\n\n      await timingCollector.flush()\n\n      expect(mockSubmitTimingReports).toHaveBeenCalledTimes(1)\n      const calls = mockSubmitTimingReports.mock.calls as any[]\n      const reports = calls[0][0]\n      expect(reports).toHaveLength(1)\n    })\n\n    test('should retry on flush failure', async () => {\n      mockSubmitTimingReports.mockRejectedValueOnce(new Error('Server error'))\n\n      const interactionId = 'test-interaction-1'\n      timingCollector.startInteraction(interactionId)\n      timingCollector.finalizeInteraction(interactionId)\n\n      await timingCollector.flush()\n\n      // Reports should be re-added to queue on failure\n      // Try flushing again - should retry with the same report\n      mockSubmitTimingReports.mockResolvedValueOnce({})\n      await timingCollector.flush()\n\n      expect(mockSubmitTimingReports).toHaveBeenCalledTimes(2)\n    })\n\n    test('should send reports via grpc client', async () => {\n      const interactionId = 'test-interaction-1'\n      timingCollector.startInteraction(interactionId)\n      timingCollector.finalizeInteraction(interactionId)\n\n      await timingCollector.flush()\n\n      expect(mockSubmitTimingReports).toHaveBeenCalled()\n      const calls = mockSubmitTimingReports.mock.calls as any[]\n      const reports = calls[0][0]\n      expect(reports).toBeDefined()\n      expect(Array.isArray(reports)).toBe(true)\n    })\n  })\n\n  describe('Multiple Interactions', () => {\n    test('should handle multiple interactions correctly', async () => {\n      const interactionId1 = 'test-interaction-1'\n      const interactionId2 = 'test-interaction-2'\n\n      timingCollector.startInteraction(interactionId1)\n      timingCollector.startInteraction(interactionId2)\n      timingCollector.finalizeInteraction(interactionId2)\n\n      await timingCollector.flush()\n\n      // Should have sent one report (only interactionId2 was finalized)\n      expect(mockSubmitTimingReports).toHaveBeenCalledTimes(1)\n      const calls = mockSubmitTimingReports.mock.calls as any[]\n      const reports = calls[0][0]\n      expect(reports).toHaveLength(1)\n      expect(reports[0].interactionId).toBe(interactionId2)\n    })\n  })\n})\n"
  },
  {
    "path": "lib/main/timing/TimingCollector.ts",
    "content": "import { getCurrentUserId, store } from '../store'\nimport { platform, hostname, arch } from 'os'\nimport { performance } from 'perf_hooks'\nimport { app } from 'electron'\nimport { TimingReport, TimingEvent } from '@/app/generated/ito_pb'\nimport { grpcClient } from '../../clients/grpcClient'\nimport { interactionManager } from '../interactions/InteractionManager'\nimport { STORE_KEYS } from '../../constants/store-keys'\n\n/**\n * Enum for all tracked timing events in the interaction lifecycle\n */\nexport enum TimingEventName {\n  // Core interaction events\n  INTERACTION_ACTIVE = 'interaction_active',\n\n  // Server communication\n  SERVER_DICTATION = 'server_transcribe',\n  SERVER_EDITING = 'server_editing',\n\n  // Context and processing\n  SELCTED_TEXT_GATHER = 'selected_text_gather',\n  WINDOW_CONTEXT_GATHER = 'window_context_gather',\n  GRAMMAR_SERVICE = 'grammar_service',\n  CURSOR_CONTEXT_GATHER = 'cursor_context_gather',\n\n  // Output\n  TEXT_WRITER = 'text_writer',\n}\n\ninterface ActiveTiming {\n  interactionId: string\n  startTimestamp: string\n  events: Map<TimingEventName, TimingEvent>\n}\n\n/**\n * TimingCollector service for collecting and submitting interaction timing data\n * Only collects data if analytics are enabled\n */\nexport class TimingCollector {\n  private activeTimings = new Map<string, ActiveTiming>()\n  private completedReports: TimingReport[] = []\n  private flushTimer: NodeJS.Timeout | null = null\n  private FIRST_EVENT = TimingEventName.INTERACTION_ACTIVE\n\n  // Configuration\n  private readonly FLUSH_INTERVAL_MS = 5_000\n  private readonly BATCH_SIZE = 10\n  private readonly MAX_QUEUE_SIZE = 100\n\n  constructor() {\n    this.scheduleFlush()\n    console.log('[TimingCollector] Service initialized')\n  }\n\n  private shouldCollect(): boolean {\n    const settings = store.get(STORE_KEYS.SETTINGS)\n    const shareAnalytics = settings?.shareAnalytics ?? false\n    return shareAnalytics\n  }\n\n  /**\n   * Start a new timing session for an interaction\n   */\n  startInteraction(interactionId?: string) {\n    if (!this.shouldCollect()) {\n      return\n    }\n\n    const id = interactionId || interactionManager.getCurrentInteractionId()\n    if (!id) {\n      console.warn(\n        '[TimingCollector] Cannot start timing: no interaction ID available',\n      )\n      return\n    }\n\n    this.activeTimings.set(id, {\n      interactionId: id,\n      startTimestamp: new Date().toISOString(),\n      events: new Map(),\n    })\n  }\n\n  startTiming(eventName: TimingEventName, interactionId?: string) {\n    if (!this.shouldCollect()) {\n      return\n    }\n\n    const id = interactionId || interactionManager.getCurrentInteractionId()\n\n    if (!id) {\n      return\n    }\n\n    const active = this.activeTimings.get(id)\n    if (!active) {\n      console.warn(\n        `[TimingCollector] Cannot start timing for unknown interaction: ${id}`,\n      )\n      return\n    }\n\n    const timingEvent = {\n      name: eventName,\n      startMs: performance.now(),\n    } as TimingEvent\n    active.events.set(eventName, timingEvent)\n  }\n\n  endTiming(eventName: TimingEventName, interactionId?: string) {\n    if (!this.shouldCollect()) {\n      return\n    }\n\n    const id = interactionId || interactionManager.getCurrentInteractionId()\n\n    if (!id) {\n      return\n    }\n\n    const active = this.activeTimings.get(id)\n    if (!active) {\n      console.warn(\n        `[TimingCollector] Cannot end timing for unknown interaction: ${id}`,\n      )\n      return\n    }\n\n    const timingEvent = active.events.get(eventName)\n    if (!timingEvent) {\n      console.warn(\n        `[TimingCollector] Cannot end timing for unknown event: ${eventName}`,\n      )\n      return\n    }\n\n    timingEvent.endMs = performance.now()\n    timingEvent.durationMs = timingEvent.endMs - timingEvent.startMs\n  }\n\n  /**\n   * Finalize an interaction and move it to completed reports\n   * If no interactionId is provided, uses the current interaction from interactionManager\n   */\n  finalizeInteraction(interactionId?: string) {\n    if (!this.shouldCollect()) {\n      return\n    }\n\n    const id = interactionId || interactionManager.getCurrentInteractionId()\n    if (!id) {\n      console.warn(\n        '[TimingCollector] Cannot finalize: no interaction ID available',\n      )\n      return\n    }\n\n    const active = this.activeTimings.get(id)\n    if (!active) {\n      console.warn(\n        `[TimingCollector] Cannot finalize unknown interaction: ${id}`,\n      )\n      return\n    }\n\n    // Calculate total duration\n    const events = Array.from(active.events.values())\n    const firstEvent = events.find(e => e.name === this.FIRST_EVENT)\n    const lastEvent = events.reduce((latest, event) => {\n      const eventEnd = event.endMs || event.startMs\n      const latestEnd = latest.endMs || latest.startMs\n      return eventEnd > latestEnd ? event : latest\n    }, events[0])\n\n    const totalDuration = firstEvent\n      ? (lastEvent.endMs || lastEvent.startMs) - firstEvent.startMs\n      : 0\n\n    // Create timing report\n    const report = {\n      interactionId: id,\n      userId: getCurrentUserId() || 'unknown',\n      platform: platform(),\n      appVersion: app.getVersion(),\n      hostname: hostname(),\n      architecture: arch(),\n      timestamp: active.startTimestamp,\n      events,\n      totalDurationMs: totalDuration,\n    } as TimingReport\n\n    // Remove from active and add to completed\n    this.activeTimings.delete(id)\n    this.completedReports.push(report)\n\n    // Enforce max queue size\n    if (this.completedReports.length > this.MAX_QUEUE_SIZE) {\n      console.warn(\n        `[TimingCollector] Queue size exceeded ${this.MAX_QUEUE_SIZE}, dropping oldest reports`,\n      )\n      this.completedReports = this.completedReports.slice(-this.MAX_QUEUE_SIZE)\n    }\n\n    console.log(\n      `[TimingCollector] Finalized interaction: ${id} (${events.length} events, ${totalDuration}ms total)`,\n    )\n\n    // Check if we should flush\n    if (this.completedReports.length >= this.BATCH_SIZE) {\n      this.flush()\n    }\n  }\n\n  /**\n   * Clear an interaction without finalizing (for errors/cancellations)\n   * If no interactionId is provided, uses the current interaction from interactionManager\n   */\n  clearInteraction(interactionId?: string) {\n    const id = interactionId || interactionManager.getCurrentInteractionId()\n    if (!id) {\n      return\n    }\n\n    this.activeTimings.delete(id)\n    console.log(`[TimingCollector] Cleared interaction: ${id}`)\n  }\n\n  /**\n   * Flush completed reports to the server via gRPC\n   */\n  async flush({ flushAll = false } = {}) {\n    if (this.completedReports.length === 0) {\n      return\n    }\n\n    const reportsToSend = this.completedReports.splice(\n      0,\n      flushAll ? this.completedReports.length : this.BATCH_SIZE,\n    )\n\n    console.log(\n      `[TimingCollector] Flushing ${reportsToSend.length} timing reports to server`,\n    )\n\n    try {\n      await grpcClient.submitTimingReports(reportsToSend)\n\n      console.log(\n        `[TimingCollector] Successfully submitted ${reportsToSend.length} reports`,\n      )\n    } catch (error) {\n      console.error('[TimingCollector] Failed to submit timing data:', error)\n      // Re-add reports to the front of the queue for retry\n      this.completedReports.unshift(...reportsToSend)\n    }\n  }\n\n  /**\n   * Schedule periodic flushing\n   */\n  private scheduleFlush() {\n    if (!this.flushTimer) {\n      this.flushTimer = setInterval(() => {\n        this.flush()\n      }, this.FLUSH_INTERVAL_MS)\n    }\n  }\n\n  /**\n   * Stop periodic flushing and flush any remaining reports\n   */\n  async shutdown() {\n    if (this.flushTimer) {\n      clearInterval(this.flushTimer)\n      this.flushTimer = null\n    }\n\n    // Flush any remaining reports\n    await this.flush({ flushAll: true })\n\n    console.log('[TimingCollector] Service shutdown complete')\n  }\n\n  /**\n   * Utility function to wrap an async operation with automatic timing\n   * Handles both successful and error cases automatically\n   *\n   * @example\n   * const result = await timingCollector.timeAsync(\n   *   interactionId,\n   *   TimingEventName.TEXT_WRITER,\n   *   async () => await setFocusedText(transcript)\n   * )\n   */\n  async timeAsync<T>(\n    eventName: TimingEventName,\n    fn: () => Promise<T> | T,\n    interactionId?: string,\n  ): Promise<T> {\n    this.startTiming(eventName, interactionId)\n    try {\n      const result = await fn()\n      return result\n    } finally {\n      // Always end timing, even if the function throws\n      this.endTiming(eventName, interactionId)\n    }\n  }\n}\n\nexport const timingCollector = new TimingCollector()\n"
  },
  {
    "path": "lib/main/tray.ts",
    "content": "import { app, Menu, Tray, nativeImage } from 'electron'\nimport { join } from 'path'\nimport { audioRecorderService } from '../media/audio'\nimport store, { SettingsStore } from './store'\nimport { STORE_KEYS } from '../constants/store-keys'\nimport { createAppWindow, mainWindow } from './app'\nimport { voiceInputService } from './voiceInputService'\n\nlet tray: Tray | null = null\nconst TRAY_GUID = '7c6b7a2e-0d7e-4a4a-9d3d-2a3d9b6f2b10' // This is a GUID for the tray icon, ensures that the icon maintains position across restarts\nconst TRAY_HEIGHT = 16\n\nfunction getTrayIconPath(): string {\n  // Use the repo resource path in dev and the app resources path in prod\n  if (!app.isPackaged) {\n    return join(__dirname, '../../resources/build/ito-logo.png')\n  }\n  return join(process.resourcesPath, 'build', 'ito-logo.png')\n}\n\nasync function buildMicrophoneSubmenu(): Promise<\n  Electron.MenuItemConstructorOptions[]\n> {\n  const settings = store.get(STORE_KEYS.SETTINGS) as SettingsStore\n  const currentDeviceId = settings.microphoneDeviceId\n\n  let devices: string[] = []\n  try {\n    devices = await audioRecorderService.getDeviceList()\n  } catch {\n    devices = []\n  }\n\n  const onSelect = (deviceId: string, label: string) => {\n    const prev = store.get(STORE_KEYS.SETTINGS) as SettingsStore\n    const updated: SettingsStore = {\n      ...prev,\n      microphoneDeviceId: deviceId,\n      microphoneName: label,\n    }\n    store.set(STORE_KEYS.SETTINGS, updated)\n    voiceInputService.handleMicrophoneChanged(deviceId)\n    // Rebuild the context menu to update the checked item\n    void rebuildTrayMenu()\n  }\n\n  const items: Electron.MenuItemConstructorOptions[] = [\n    {\n      label: 'Auto-detect',\n      type: 'radio',\n      checked: currentDeviceId === 'default',\n      click: () => onSelect('default', 'Auto-detect'),\n    },\n  ]\n\n  for (const deviceName of devices) {\n    items.push({\n      label: deviceName,\n      type: 'radio',\n      checked: currentDeviceId === deviceName,\n      click: () => onSelect(deviceName, deviceName),\n    })\n  }\n\n  items.push({ type: 'separator' })\n  items.push({\n    label: 'Refresh devices',\n    click: () => {\n      void rebuildTrayMenu()\n    },\n  })\n\n  return items\n}\n\nasync function rebuildTrayMenu(): Promise<void> {\n  if (!tray) return\n\n  const micSubmenu = await buildMicrophoneSubmenu()\n\n  const template: Electron.MenuItemConstructorOptions[] = [\n    {\n      label: 'Open Dashboard',\n      click: () => {\n        if (!mainWindow) {\n          createAppWindow()\n        } else {\n          if (!mainWindow.isVisible()) mainWindow.show()\n          mainWindow.focus()\n        }\n      },\n    },\n    {\n      label: 'Select Microphone',\n      submenu: micSubmenu,\n    },\n    { type: 'separator' },\n    {\n      label: 'Quit Ito',\n      role: 'quit',\n    },\n  ]\n\n  const menu = Menu.buildFromTemplate(template)\n  tray.setContextMenu(menu)\n}\n\nexport async function createAppTray(): Promise<void> {\n  if (tray) return\n\n  const iconPath = getTrayIconPath()\n\n  let image = nativeImage.createFromPath(iconPath)\n\n  if (image.isEmpty() && process.platform === 'darwin') {\n    image = nativeImage.createFromNamedImage('NSImageNameStatusAvailable')\n  }\n\n  const trayImage = image.resize({ height: TRAY_HEIGHT })\n\n  tray = new Tray(trayImage, TRAY_GUID)\n  tray.setToolTip('Ito')\n\n  await rebuildTrayMenu()\n\n  // For Windows, manually pop the menu. On macOS, rely on native menu so the icon stays highlighted.\n  if (process.platform !== 'darwin') {\n    tray.on('click', async () => {\n      await rebuildTrayMenu()\n      tray?.popUpContextMenu()\n    })\n\n    tray.on('right-click', async () => {\n      await rebuildTrayMenu()\n      tray?.popUpContextMenu()\n    })\n  }\n}\n\nexport function destroyAppTray(): void {\n  if (tray) {\n    tray.destroy()\n    tray = null\n  }\n}\n"
  },
  {
    "path": "lib/main/voiceInputService.test.ts",
    "content": "import { describe, test, expect, beforeEach, mock } from 'bun:test'\n\n// Mock audio recorder service\nconst mockAudioRecorderService = {\n  startRecording: mock(),\n  stopRecording: mock(),\n  on: mock(),\n  initialize: mock(),\n  requestDeviceConfig: mock(),\n  awaitDrainComplete: mock(() => Promise.resolve()),\n}\nmock.module('../media/audio', () => ({\n  audioRecorderService: mockAudioRecorderService,\n}))\n\n// Mock system audio control\nconst mockMuteSystemAudio = mock()\nconst mockUnmuteSystemAudio = mock()\nmock.module('../media/systemAudio', () => ({\n  muteSystemAudio: mockMuteSystemAudio,\n  unmuteSystemAudio: mockUnmuteSystemAudio,\n}))\n\n// Mock electron windows\nconst mockPillWindow = {\n  webContents: {\n    send: mock(),\n    isDestroyed: mock(() => false),\n  },\n  isDestroyed: mock(() => false),\n}\nconst mockMainWindow = {\n  webContents: {\n    send: mock(),\n    isDestroyed: mock(() => false),\n  },\n  isDestroyed: mock(() => false),\n}\nmock.module('./app', () => ({\n  getPillWindow: mock(() => mockPillWindow),\n  mainWindow: mockMainWindow,\n}))\n\n// Mock electron store\nconst mockStore = {\n  get: mock(),\n}\nmock.module('./store', () => ({\n  default: mockStore,\n  getCurrentUserId: mock(() => 'test-user-123'),\n  createNewAuthState: mock(() => ({\n    state: 'test-state',\n    codeVerifier: 'test-verifier',\n  })),\n}))\n\nmock.module('electron-log', () => ({\n  default: {\n    info: mock(),\n    warn: mock(),\n    error: mock(),\n  },\n}))\n\n// Mock console to avoid noise\nbeforeEach(() => {\n  console.log = mock()\n  console.error = mock()\n  console.info = mock()\n})\n\nimport { voiceInputService } from './voiceInputService'\nimport { STORE_KEYS } from '../constants/store-keys'\n\ndescribe('VoiceInputService', () => {\n  beforeEach(() => {\n    // Reset all mocks\n    mockAudioRecorderService.startRecording.mockClear()\n    mockAudioRecorderService.stopRecording.mockClear()\n    mockAudioRecorderService.on.mockClear()\n    mockAudioRecorderService.initialize.mockClear()\n    mockAudioRecorderService.requestDeviceConfig.mockClear()\n    mockAudioRecorderService.awaitDrainComplete.mockClear()\n    mockAudioRecorderService.awaitDrainComplete.mockResolvedValue(undefined)\n\n    mockMuteSystemAudio.mockClear()\n    mockUnmuteSystemAudio.mockClear()\n\n    mockPillWindow.webContents.send.mockClear()\n    mockMainWindow.webContents.send.mockClear()\n\n    mockStore.get.mockClear()\n\n    // Setup default store values\n    mockStore.get.mockImplementation((key: string) => {\n      if (key === STORE_KEYS.SETTINGS) {\n        return {\n          microphoneDeviceId: 'test-device-123',\n          muteAudioWhenDictating: false,\n        }\n      }\n      return null\n    })\n  })\n\n  describe('Audio Recording Lifecycle', () => {\n    test('should start audio recording with device from settings', () => {\n      const testDeviceId = 'test-microphone-device'\n      mockStore.get.mockReturnValue({\n        microphoneDeviceId: testDeviceId,\n        muteAudioWhenDictating: false,\n      })\n\n      voiceInputService.startAudioRecording()\n\n      expect(mockAudioRecorderService.startRecording).toHaveBeenCalledWith(\n        testDeviceId,\n      )\n      expect(mockMuteSystemAudio).not.toHaveBeenCalled()\n    })\n\n    test('should mute system audio when configured', () => {\n      mockStore.get.mockReturnValue({\n        microphoneDeviceId: 'test-device',\n        muteAudioWhenDictating: true,\n      })\n\n      voiceInputService.startAudioRecording()\n\n      expect(mockMuteSystemAudio).toHaveBeenCalledTimes(1)\n      expect(mockAudioRecorderService.startRecording).toHaveBeenCalledWith(\n        'test-device',\n      )\n    })\n\n    test('should stop audio recording and wait for drain', async () => {\n      mockStore.get.mockReturnValue({\n        muteAudioWhenDictating: false,\n      })\n\n      await voiceInputService.stopAudioRecording()\n\n      expect(mockAudioRecorderService.stopRecording).toHaveBeenCalledTimes(1)\n      expect(mockAudioRecorderService.awaitDrainComplete).toHaveBeenCalledWith(\n        500,\n      )\n      expect(mockUnmuteSystemAudio).not.toHaveBeenCalled()\n    })\n\n    test('should unmute system audio when stopping if it was muted', async () => {\n      mockStore.get.mockReturnValue({\n        muteAudioWhenDictating: true,\n      })\n\n      await voiceInputService.stopAudioRecording()\n\n      expect(mockAudioRecorderService.stopRecording).toHaveBeenCalledTimes(1)\n      expect(mockUnmuteSystemAudio).toHaveBeenCalledTimes(1)\n    })\n\n    test('should handle drain timeout gracefully', async () => {\n      mockAudioRecorderService.awaitDrainComplete.mockRejectedValueOnce(\n        new Error('Drain timeout'),\n      )\n      mockStore.get.mockReturnValue({\n        muteAudioWhenDictating: false,\n      })\n\n      await voiceInputService.stopAudioRecording()\n\n      expect(mockAudioRecorderService.stopRecording).toHaveBeenCalledTimes(1)\n      // Should not throw and continue with cleanup\n    })\n  })\n\n  describe('Audio Recorder Listeners', () => {\n    test('should set up volume update listener', () => {\n      voiceInputService.setUpAudioRecorderListeners()\n\n      expect(mockAudioRecorderService.on).toHaveBeenCalledWith(\n        'volume-update',\n        expect.any(Function),\n      )\n      expect(mockAudioRecorderService.on).toHaveBeenCalledWith(\n        'error',\n        expect.any(Function),\n      )\n      expect(mockAudioRecorderService.initialize).toHaveBeenCalledTimes(1)\n    })\n\n    test('should broadcast volume updates to windows', () => {\n      voiceInputService.setUpAudioRecorderListeners()\n\n      const volumeUpdateHandler = mockAudioRecorderService.on.mock.calls.find(\n        call => call[0] === 'volume-update',\n      )?.[1]\n\n      expect(volumeUpdateHandler).toBeDefined()\n\n      const testVolume = 0.75\n      volumeUpdateHandler(testVolume)\n\n      expect(mockPillWindow.webContents.send).toHaveBeenCalledWith(\n        'volume-update',\n        testVolume,\n      )\n      expect(mockMainWindow.webContents.send).toHaveBeenCalledWith(\n        'volume-update',\n        testVolume,\n      )\n    })\n\n    test('should handle audio recorder errors gracefully', () => {\n      voiceInputService.setUpAudioRecorderListeners()\n\n      const errorHandler = mockAudioRecorderService.on.mock.calls.find(\n        call => call[0] === 'error',\n      )?.[1]\n\n      expect(errorHandler).toBeDefined()\n\n      const testError = new Error('Microphone access denied')\n      errorHandler(testError)\n\n      // Error should be logged - test passes if no exception is thrown\n      expect(true).toBe(true)\n    })\n\n    test('should not send to main window when it is destroyed', () => {\n      voiceInputService.setUpAudioRecorderListeners()\n\n      const volumeUpdateHandler = mockAudioRecorderService.on.mock.calls.find(\n        call => call[0] === 'volume-update',\n      )?.[1]\n\n      expect(volumeUpdateHandler).toBeDefined()\n\n      mockMainWindow.isDestroyed.mockReturnValueOnce(true)\n\n      const testVolume = 0.42\n      volumeUpdateHandler(testVolume)\n\n      expect(mockPillWindow.webContents.send).toHaveBeenCalledWith(\n        'volume-update',\n        testVolume,\n      )\n      expect(mockMainWindow.webContents.send).not.toHaveBeenCalled()\n    })\n\n    test('should not send to main window when webContents is destroyed', () => {\n      voiceInputService.setUpAudioRecorderListeners()\n\n      const volumeUpdateHandler = mockAudioRecorderService.on.mock.calls.find(\n        call => call[0] === 'volume-update',\n      )?.[1]\n\n      expect(volumeUpdateHandler).toBeDefined()\n\n      mockMainWindow.webContents.isDestroyed.mockReturnValueOnce(true)\n\n      const testVolume = 0.55\n      volumeUpdateHandler(testVolume)\n\n      expect(mockPillWindow.webContents.send).toHaveBeenCalledWith(\n        'volume-update',\n        testVolume,\n      )\n      expect(mockMainWindow.webContents.send).not.toHaveBeenCalled()\n    })\n  })\n\n  describe('Device Management', () => {\n    test('should use device ID from settings', () => {\n      const customDeviceId = 'custom-microphone-device-456'\n      mockStore.get.mockReturnValue({\n        microphoneDeviceId: customDeviceId,\n        muteAudioWhenDictating: false,\n      })\n\n      voiceInputService.startAudioRecording()\n\n      expect(mockAudioRecorderService.startRecording).toHaveBeenCalledWith(\n        customDeviceId,\n      )\n    })\n\n    test('should handle missing device ID gracefully', () => {\n      mockStore.get.mockReturnValue({\n        // Missing microphoneDeviceId\n        muteAudioWhenDictating: false,\n      })\n\n      voiceInputService.startAudioRecording()\n\n      expect(mockAudioRecorderService.startRecording).toHaveBeenCalledWith(\n        undefined,\n      )\n    })\n\n    test('should handle microphone change', () => {\n      const newDeviceId = 'new-device-789'\n\n      voiceInputService.handleMicrophoneChanged(newDeviceId)\n\n      expect(mockAudioRecorderService.requestDeviceConfig).toHaveBeenCalledWith(\n        newDeviceId,\n      )\n    })\n  })\n\n  describe('Error Resilience', () => {\n    test('should continue when pill window is unavailable', async () => {\n      const mockApp: any = await import('./app')\n      mockApp.getPillWindow.mockReturnValueOnce(null)\n\n      voiceInputService.setUpAudioRecorderListeners()\n\n      const volumeUpdateHandler = mockAudioRecorderService.on.mock.calls.find(\n        call => call[0] === 'volume-update',\n      )?.[1]\n\n      // Should not crash when window is unavailable\n      expect(() => volumeUpdateHandler(0.5)).not.toThrow()\n\n      // Reset mock for future tests\n      mockApp.getPillWindow.mockReturnValue(mockPillWindow)\n    })\n\n    test('should handle multiple volume updates', () => {\n      voiceInputService.setUpAudioRecorderListeners()\n\n      const volumeUpdateHandler = mockAudioRecorderService.on.mock.calls.find(\n        call => call[0] === 'volume-update',\n      )?.[1]\n\n      volumeUpdateHandler(0.1)\n      volumeUpdateHandler(0.5)\n      volumeUpdateHandler(0.9)\n\n      expect(mockPillWindow.webContents.send).toHaveBeenCalledTimes(3)\n      expect(mockMainWindow.webContents.send).toHaveBeenCalledTimes(3)\n    })\n  })\n\n  describe('Integration Scenarios', () => {\n    test('should coordinate complete recording session', async () => {\n      const deviceId = 'session-test-device'\n      mockStore.get.mockReturnValue({\n        microphoneDeviceId: deviceId,\n        muteAudioWhenDictating: true,\n      })\n\n      // Set up listeners\n      voiceInputService.setUpAudioRecorderListeners()\n\n      // Start recording\n      voiceInputService.startAudioRecording()\n\n      expect(mockMuteSystemAudio).toHaveBeenCalledTimes(1)\n      expect(mockAudioRecorderService.startRecording).toHaveBeenCalledWith(\n        deviceId,\n      )\n\n      // Simulate volume updates\n      const volumeHandler = mockAudioRecorderService.on.mock.calls.find(\n        call => call[0] === 'volume-update',\n      )?.[1]\n      volumeHandler(0.6)\n      volumeHandler(0.8)\n\n      // Stop recording\n      await voiceInputService.stopAudioRecording()\n\n      expect(mockAudioRecorderService.stopRecording).toHaveBeenCalledTimes(1)\n      expect(mockUnmuteSystemAudio).toHaveBeenCalledTimes(1)\n      expect(mockPillWindow.webContents.send).toHaveBeenCalledWith(\n        'volume-update',\n        0.6,\n      )\n      expect(mockPillWindow.webContents.send).toHaveBeenCalledWith(\n        'volume-update',\n        0.8,\n      )\n    })\n\n    test('should handle multiple start/stop cycles', async () => {\n      mockStore.get.mockReturnValue({\n        microphoneDeviceId: 'test-device',\n        muteAudioWhenDictating: false,\n      })\n\n      // First cycle\n      voiceInputService.startAudioRecording()\n      await voiceInputService.stopAudioRecording()\n\n      // Second cycle\n      voiceInputService.startAudioRecording()\n      await voiceInputService.stopAudioRecording()\n\n      expect(mockAudioRecorderService.startRecording).toHaveBeenCalledTimes(2)\n      expect(mockAudioRecorderService.stopRecording).toHaveBeenCalledTimes(2)\n    })\n  })\n})\n"
  },
  {
    "path": "lib/main/voiceInputService.ts",
    "content": "import { audioRecorderService } from '../media/audio'\nimport { muteSystemAudio, unmuteSystemAudio } from '../media/systemAudio'\nimport { getPillWindow, mainWindow } from './app'\nimport store from './store'\nimport { STORE_KEYS } from '../constants/store-keys'\nimport { IPC_EVENTS } from '../types/ipc'\nimport log from 'electron-log'\n\nexport class VoiceInputService {\n  /**\n   * Starts audio recording and handles system audio muting.\n   * Does NOT start the ItoStreamController - that should be done separately.\n   */\n  public startAudioRecording = () => {\n    console.log('[VoiceInputService] Starting audio recording')\n\n    const settings = store.get(STORE_KEYS.SETTINGS)\n    const deviceId = settings.microphoneDeviceId\n\n    // Mute system audio if needed\n    if (settings.muteAudioWhenDictating) {\n      console.log('[VoiceInputService] Muting system audio for dictation')\n      muteSystemAudio()\n    }\n\n    // Start audio recorder\n    console.log(\n      '[VoiceInputService] Starting audio recorder with device:',\n      deviceId,\n    )\n    audioRecorderService.startRecording(deviceId)\n\n    console.log('[VoiceInputService] Audio recording started')\n  }\n\n  /**\n   * Stops audio recording and handles system audio unmuting.\n   * Waits for the audio recorder to drain before returning.\n   */\n  public stopAudioRecording = async () => {\n    console.log('[VoiceInputService] Stopping audio recording')\n    audioRecorderService.stopRecording()\n    console.log(\n      '[VoiceInputService] Audio recorder stopped, waiting for drain...',\n    )\n\n    // Wait for explicit drain-complete signal from the recorder (with timeout fallback)\n    try {\n      await (audioRecorderService as any).awaitDrainComplete?.(500)\n      console.log('[VoiceInputService] Drain complete')\n    } catch (e) {\n      log.warn('[VoiceInputService] drain-complete wait failed, proceeding:', e)\n    }\n\n    // Unmute system audio if it was muted\n    if (store.get(STORE_KEYS.SETTINGS).muteAudioWhenDictating) {\n      console.log('[VoiceInputService] Unmuting system audio after dictation')\n      unmuteSystemAudio()\n    }\n\n    console.log('[VoiceInputService] Audio recording stopped')\n  }\n\n  public setUpAudioRecorderListeners = () => {\n    // Note: audio-chunk and audio-config are now handled directly by ItoStreamController\n    // when the gRPC stream starts. VoiceInputService only handles UI-related events.\n\n    audioRecorderService.on('volume-update', volume => {\n      getPillWindow()?.webContents.send(IPC_EVENTS.VOLUME_UPDATE, volume)\n      if (\n        mainWindow &&\n        !mainWindow.isDestroyed() &&\n        !mainWindow.webContents.isDestroyed()\n      ) {\n        mainWindow.webContents.send(IPC_EVENTS.VOLUME_UPDATE, volume)\n      }\n    })\n\n    audioRecorderService.on('error', err => {\n      // Handle errors, maybe show a dialog to the user\n      log.error('[VoiceInputService] Audio recorder error:', err.message)\n    })\n\n    audioRecorderService.initialize()\n  }\n\n  /**\n   * Call this when microphone selection changes to update the transcription\n   * config with the effective output sample rate for the chosen device.\n   */\n  public handleMicrophoneChanged = (deviceId: string) => {\n    audioRecorderService.requestDeviceConfig(deviceId)\n  }\n}\n\nexport const voiceInputService = new VoiceInputService()\n"
  },
  {
    "path": "lib/media/IAccessibilityContextProvider.ts",
    "content": "import type {\n  CursorContextOptions,\n  CursorContextResult,\n} from '../types/cursorContext'\n\nexport interface IAccessibilityContextProvider {\n  initialize(): void\n\n  shutdown(): void\n\n  isRunning(): boolean\n\n  getCursorContext(options?: CursorContextOptions): Promise<CursorContextResult>\n}\n"
  },
  {
    "path": "lib/media/active-application.test.ts",
    "content": "import { describe, expect, mock, test, beforeEach, afterEach } from 'bun:test'\nimport { ActiveWindow } from './active-application'\n\nconst mockActiveWindow: ActiveWindow = {\n  title: 'Test Window',\n  appName: 'Test App',\n  windowId: 123,\n  processId: 456,\n  positon: {\n    x: 100,\n    y: 200,\n    width: 800,\n    height: 600,\n  },\n}\nconst mockPathToBinary = '/path/to/binary'\nconst mockGetNativeBinaryPath = mock()\nmock.module('./native-interface', () => ({\n  getNativeBinaryPath: mockGetNativeBinaryPath,\n}))\nconst mockExecFile = mock(\n  (\n    _: string,\n    callback: (err: Error | null, stdout: string, stderr: string) => void,\n  ) => {\n    callback(null, JSON.stringify(mockActiveWindow), '')\n  },\n)\nmock.module('child_process', () => ({\n  execFile: mockExecFile,\n}))\n\n// Mock console to avoid spam\nbeforeEach(() => {\n  console.error = mock()\n})\n\ndescribe('active-application', () => {\n  beforeEach(() => {\n    mockGetNativeBinaryPath.mockReturnValue(mockPathToBinary)\n  })\n\n  afterEach(() => {\n    mockGetNativeBinaryPath.mockReset()\n    mockExecFile.mockReset()\n  })\n\n  test('should return active window info when successful', async () => {\n    mockGetNativeBinaryPath.mockReturnValue(mockPathToBinary)\n    mockExecFile.mockImplementation((_: string, callback) => {\n      callback(null, JSON.stringify(mockActiveWindow), '')\n    })\n    const { getActiveWindow } = await import('./active-application')\n\n    const result = await getActiveWindow()\n\n    expect(result).toEqual(mockActiveWindow)\n    expect(mockGetNativeBinaryPath).toHaveBeenCalledWith('active-application')\n    expect(mockExecFile).toHaveBeenCalledWith(\n      mockPathToBinary,\n      expect.any(Function),\n    )\n  })\n\n  test('should return null when binary path is not found', async () => {\n    mockGetNativeBinaryPath.mockReturnValue(null)\n    const { getActiveWindow } = await import('./active-application')\n\n    const result = await getActiveWindow()\n\n    expect(result).toBeNull()\n    expect(mockExecFile).not.toHaveBeenCalled()\n  })\n\n  test('should return null when execFile fails', async () => {\n    mockGetNativeBinaryPath.mockReturnValue(mockPathToBinary)\n    mockExecFile.mockImplementation((_: string, callback) => {\n      callback(new Error('Binary failed'), '', 'Error message')\n    })\n    const { getActiveWindow } = await import('./active-application')\n\n    const result = await getActiveWindow()\n\n    expect(result).toBeNull()\n  })\n\n  test('should parse JSON correctly from stdout', async () => {\n    mockGetNativeBinaryPath.mockReturnValue(mockPathToBinary)\n    mockExecFile.mockImplementation((_: string, callback) => {\n      callback(null, `  ${JSON.stringify(mockActiveWindow)}  \\n`, '')\n    })\n    const { getActiveWindow } = await import('./active-application')\n\n    const result = await getActiveWindow()\n\n    expect(result).toEqual(mockActiveWindow)\n  })\n})\n"
  },
  {
    "path": "lib/media/active-application.ts",
    "content": "import { execFile } from 'child_process'\nimport { getNativeBinaryPath } from './native-interface'\n\nconst nativeModuleName = 'active-application'\n\nexport type ActiveWindow = {\n  title: string\n  appName: string\n  windowId: number\n  processId: number\n  positon: {\n    x: number\n    y: number\n    width: number\n    height: number\n  }\n}\n\nexport async function getActiveWindow(): Promise<ActiveWindow | null> {\n  const path = getNativeBinaryPath(nativeModuleName)\n  if (!path) {\n    console.error(`Cannot determine ${nativeModuleName} binary path`)\n    return null\n  }\n\n  const result = (await new Promise(resolve => {\n    execFile(path, (err, stdout, stderr) => {\n      if (err) {\n        console.error(`${nativeModuleName} error:`, err, stderr)\n        return resolve('null')\n      }\n      return resolve(stdout.trim())\n    })\n  })) as string\n\n  if (result) {\n    return JSON.parse(result) as ActiveWindow\n  } else {\n    return null\n  }\n}\n"
  },
  {
    "path": "lib/media/audio.test.ts",
    "content": "import { describe, test, expect, beforeEach, mock } from 'bun:test'\nimport { EventEmitter } from 'events'\n\n// Mock all external dependencies\nconst mockSpawn = mock(() => mockChildProcess)\nconst mockChildProcess = {\n  stdin: {\n    write: mock(),\n  },\n  stdout: new EventEmitter(),\n  stderr: new EventEmitter(),\n  on: mock(\n    (\n      event: string,\n      handler: ((code: number) => void) | ((err: Error) => void),\n    ) => {\n      // Capture the event handlers so we can trigger them in tests\n      if (event === 'close') {\n        mockChildProcess._closeHandler = handler as (code: number) => void\n      } else if (event === 'error') {\n        mockChildProcess._errorHandler = handler as (err: Error) => void\n      }\n    },\n  ),\n  kill: mock(),\n  pid: 12345,\n  _closeHandler: null as ((code: number) => void) | null,\n  _errorHandler: null as ((err: Error) => void) | null,\n}\n\nmock.module('child_process', () => ({\n  spawn: mockSpawn,\n}))\n\nmock.module('path', () => ({\n  join: mock((...paths: string[]) => paths.join('/')),\n}))\n\nmock.module('electron', () => ({\n  app: {\n    isPackaged: false,\n    getPath: (type: string) =>\n      type === 'userData' ? '/tmp/test-ito-app' : '/tmp',\n  },\n}))\n\nmock.module('os', () => ({\n  default: {\n    platform: mock(() => 'darwin'),\n    arch: mock(() => 'arm64'),\n  },\n}))\n\n// Helper function to wait for async operations\nconst waitForProcessing = () => new Promise(resolve => setTimeout(resolve, 10))\n\n// Import after mocking\nimport { audioRecorderService } from './audio'\n\ndescribe('AudioRecorderService', () => {\n  beforeEach(() => {\n    // Reset all mocks\n    mockSpawn.mockClear()\n    mockSpawn.mockReturnValue(mockChildProcess)\n    mockChildProcess.stdin.write.mockClear()\n    mockChildProcess.on.mockClear()\n    mockChildProcess.kill.mockClear()\n\n    // Reset child process to clean state\n    mockChildProcess.stdout.removeAllListeners()\n    mockChildProcess.stderr.removeAllListeners()\n    mockChildProcess._closeHandler = null\n    mockChildProcess._errorHandler = null\n\n    const events = [\n      'started',\n      'stopped',\n      'error',\n      'volume-update',\n      'audio-chunk',\n    ]\n    events.forEach(event => {\n      audioRecorderService.removeAllListeners(event)\n    })\n\n    // Since we can't directly access private fields, we'll terminate the service\n    // to reset its state, then initialize it fresh for each test\n    audioRecorderService.terminate()\n  })\n\n  describe('Initialization Business Logic', () => {\n    test('should prevent multiple initialization', () => {\n      // First initialization\n      audioRecorderService.initialize()\n      mockSpawn.mockClear()\n\n      // Second initialization should be ignored\n      audioRecorderService.initialize()\n\n      expect(mockSpawn).not.toHaveBeenCalled()\n    })\n\n    test('should handle spawn errors gracefully', async () => {\n      const spawnError = new Error('Spawn failed')\n\n      let errorEmitted = false\n      audioRecorderService.on('error', () => {\n        errorEmitted = true\n      })\n\n      // Set up spawn to throw an error\n      mockSpawn.mockImplementationOnce(() => {\n        throw spawnError\n      })\n\n      audioRecorderService.initialize()\n\n      // Wait for error handling to complete\n      await waitForProcessing()\n\n      expect(errorEmitted).toBe(true)\n\n      // Reset the mock back to normal behavior for other tests\n      mockSpawn.mockImplementation(() => mockChildProcess)\n    })\n  })\n\n  describe('Process Lifecycle Business Logic', () => {\n    beforeEach(() => {\n      audioRecorderService.initialize()\n    })\n\n    test('should handle process close event correctly', async () => {\n      let stoppedEmitted = false\n      audioRecorderService.on('stopped', () => {\n        stoppedEmitted = true\n      })\n\n      // Simulate process close using captured handler\n      expect(mockChildProcess._closeHandler).toBeDefined()\n      mockChildProcess._closeHandler!(0)\n\n      expect(stoppedEmitted).toBe(true)\n    })\n\n    test('should handle process error event', async () => {\n      const processError = new Error('Process error')\n      let errorEmitted = false\n      audioRecorderService.on('error', () => {\n        errorEmitted = true\n      })\n\n      // Simulate process error using captured handler\n      expect(mockChildProcess._errorHandler).toBeDefined()\n      mockChildProcess._errorHandler!(processError)\n\n      expect(errorEmitted).toBe(true)\n    })\n  })\n\n  describe('Recording Commands Business Logic', () => {\n    beforeEach(() => {\n      audioRecorderService.initialize()\n    })\n\n    test('should send start recording command with device name', () => {\n      const deviceName = 'Built-in Microphone'\n\n      audioRecorderService.startRecording(deviceName)\n\n      expect(mockChildProcess.stdin.write).toHaveBeenCalledWith(\n        JSON.stringify({ command: 'start', device_name: deviceName }) + '\\n',\n      )\n    })\n\n    test('should send stop recording command', () => {\n      audioRecorderService.stopRecording()\n\n      expect(mockChildProcess.stdin.write).toHaveBeenCalledWith(\n        JSON.stringify({ command: 'stop' }) + '\\n',\n      )\n    })\n  })\n\n  describe('Device Management Business Logic', () => {\n    beforeEach(() => {\n      audioRecorderService.initialize()\n    })\n\n    test('should request device list successfully', async () => {\n      const mockDevices = ['Device 1', 'Device 2', 'Device 3']\n\n      // Setup device list promise\n      const deviceListPromise = audioRecorderService.getDeviceList()\n\n      expect(mockChildProcess.stdin.write).toHaveBeenCalledWith(\n        JSON.stringify({ command: 'list-devices' }) + '\\n',\n      )\n\n      // Simulate device list response\n      const deviceListMessage = Buffer.concat([\n        Buffer.from([1]), // MSG_TYPE_JSON\n        Buffer.from([0, 0, 0, 0]), // Length placeholder\n        Buffer.from(\n          JSON.stringify({ type: 'device-list', devices: mockDevices }),\n        ),\n      ])\n      // Update length field\n      deviceListMessage.writeUInt32LE(deviceListMessage.length - 5, 1)\n\n      // Emit data through stdout\n      mockChildProcess.stdout.emit('data', deviceListMessage)\n\n      const devices = await deviceListPromise\n      expect(devices).toEqual(mockDevices)\n    })\n\n    test('should reject device list when process not running', async () => {\n      audioRecorderService.terminate()\n\n      await expect(audioRecorderService.getDeviceList()).rejects.toThrow(\n        'Audio recorder process not running.',\n      )\n    })\n\n    test('should handle empty device list', async () => {\n      const deviceListPromise = audioRecorderService.getDeviceList()\n\n      // Simulate empty device list response\n      const deviceListMessage = Buffer.concat([\n        Buffer.from([1]), // MSG_TYPE_JSON\n        Buffer.from([0, 0, 0, 0]), // Length placeholder\n        Buffer.from(JSON.stringify({ type: 'device-list', devices: [] })),\n      ])\n      deviceListMessage.writeUInt32LE(deviceListMessage.length - 5, 1)\n\n      mockChildProcess.stdout.emit('data', deviceListMessage)\n\n      const devices = await deviceListPromise\n      expect(devices).toEqual([])\n    })\n\n    test('should handle malformed JSON in device list response', async () => {\n      const deviceListPromise = audioRecorderService.getDeviceList()\n\n      // Simulate malformed JSON response\n      const malformedMessage = Buffer.concat([\n        Buffer.from([1]), // MSG_TYPE_JSON\n        Buffer.from([0, 0, 0, 0]), // Length placeholder\n        Buffer.from('invalid json {'),\n      ])\n      malformedMessage.writeUInt32LE(malformedMessage.length - 5, 1)\n\n      // Emit data\n      mockChildProcess.stdout.emit('data', malformedMessage)\n\n      // The promise should be rejected due to JSON parse error\n      try {\n        await deviceListPromise\n        expect(true).toBe(false) // Should not reach here\n      } catch (error) {\n        expect(error).toBeInstanceOf(Error)\n        expect((error as Error).message).toBe('Failed to parse JSON response')\n      }\n\n      // Wait for processing\n      await waitForProcessing()\n    })\n  })\n\n  describe('Audio Data Processing Business Logic', () => {\n    beforeEach(() => {\n      audioRecorderService.initialize()\n    })\n\n    test('should process audio chunks and calculate volume', async () => {\n      let volumeUpdate: number | null = null\n      let audioChunk: Buffer | null = null\n\n      audioRecorderService.on('volume-update', (volume: number) => {\n        volumeUpdate = volume\n      })\n      audioRecorderService.on('audio-chunk', (chunk: Buffer) => {\n        audioChunk = chunk\n      })\n\n      // Create mock audio data (16-bit PCM)\n      const audioData = Buffer.alloc(1024)\n      // Fill with some sample data\n      for (let i = 0; i < audioData.length; i += 2) {\n        audioData.writeInt16LE(Math.floor(Math.random() * 32767), i)\n      }\n\n      // Create audio message\n      const audioMessage = Buffer.concat([\n        Buffer.from([2]), // MSG_TYPE_AUDIO\n        Buffer.from([0, 0, 0, 0]), // Length placeholder\n        audioData,\n      ])\n      audioMessage.writeUInt32LE(audioMessage.length - 5, 1)\n\n      mockChildProcess.stdout.emit('data', audioMessage)\n\n      // Wait for event processing\n      await waitForProcessing()\n\n      expect(volumeUpdate).toBeTypeOf('number')\n      expect(volumeUpdate!).toBeGreaterThanOrEqual(0)\n      expect(volumeUpdate!).toBeLessThanOrEqual(1)\n      expect(audioChunk!).toEqual(audioData)\n    })\n\n    test('should handle fragmented messages correctly', async () => {\n      let messageReceived = false\n      audioRecorderService.on('audio-chunk', () => {\n        messageReceived = true\n      })\n\n      // Create a message and split it\n      const audioData = Buffer.alloc(100)\n      const fullMessage = Buffer.concat([\n        Buffer.from([2]), // MSG_TYPE_AUDIO\n        Buffer.from([100, 0, 0, 0]), // Length: 100\n        audioData,\n      ])\n\n      // Send in two fragments\n      const fragment1 = fullMessage.slice(0, 50)\n      const fragment2 = fullMessage.slice(50)\n\n      mockChildProcess.stdout.emit('data', fragment1)\n      await waitForProcessing()\n      expect(messageReceived).toBe(false) // Should not be processed yet\n\n      mockChildProcess.stdout.emit('data', fragment2)\n      await waitForProcessing()\n      expect(messageReceived).toBe(true) // Now should be processed\n    })\n\n    test('should handle multiple messages in single data chunk', async () => {\n      let messageCount = 0\n      audioRecorderService.on('audio-chunk', () => {\n        messageCount++\n      })\n\n      // Create two small audio messages\n      const audioData1 = Buffer.alloc(10)\n      const audioData2 = Buffer.alloc(15)\n\n      const message1 = Buffer.concat([\n        Buffer.from([2]), // MSG_TYPE_AUDIO\n        Buffer.from([10, 0, 0, 0]), // Length: 10\n        audioData1,\n      ])\n\n      const message2 = Buffer.concat([\n        Buffer.from([2]), // MSG_TYPE_AUDIO\n        Buffer.from([15, 0, 0, 0]), // Length: 15\n        audioData2,\n      ])\n\n      // Send both messages in one chunk\n      const combinedData = Buffer.concat([message1, message2])\n      mockChildProcess.stdout.emit('data', combinedData)\n\n      // Wait for processing\n      await waitForProcessing()\n\n      expect(messageCount).toBe(2)\n    })\n\n    test('should handle invalid message types gracefully', async () => {\n      const invalidMessage = Buffer.concat([\n        Buffer.from([99]), // Invalid message type\n        Buffer.from([5, 0, 0, 0]), // Length: 5\n        Buffer.from('hello'),\n      ])\n\n      // Should not throw\n      expect(() => {\n        mockChildProcess.stdout.emit('data', invalidMessage)\n      }).not.toThrow()\n\n      // Wait for processing\n      await waitForProcessing()\n    })\n  })\n\n  describe('Volume Calculation Business Logic', () => {\n    beforeEach(() => {\n      audioRecorderService.initialize()\n    })\n\n    test('should calculate volume correctly for various inputs', async () => {\n      // Test silent audio (all zeros)\n      const silentAudio = Buffer.alloc(1024, 0)\n      let volume: number | null = null\n      audioRecorderService.on('volume-update', (v: number) => {\n        volume = v\n      })\n\n      const silentMessage = Buffer.concat([\n        Buffer.from([2]), // MSG_TYPE_AUDIO\n        Buffer.from([0, 0, 0, 0]), // Length placeholder\n        silentAudio,\n      ])\n      silentMessage.writeUInt32LE(silentMessage.length - 5, 1)\n\n      mockChildProcess.stdout.emit('data', silentMessage)\n      await waitForProcessing()\n\n      expect(volume!).toBe(0)\n\n      // Test maximum volume audio\n      const maxAudio = Buffer.alloc(1024)\n      for (let i = 0; i < maxAudio.length; i += 2) {\n        maxAudio.writeInt16LE(32767, i) // Max 16-bit value\n      }\n\n      const maxMessage = Buffer.concat([\n        Buffer.from([2]), // MSG_TYPE_AUDIO\n        Buffer.from([0, 0, 0, 0]), // Length placeholder\n        maxAudio,\n      ])\n      maxMessage.writeUInt32LE(maxMessage.length - 5, 1)\n\n      mockChildProcess.stdout.emit('data', maxMessage)\n      await waitForProcessing()\n\n      expect(volume!).toBe(1.0)\n    })\n\n    test('should handle empty audio buffer', async () => {\n      let volume: number | null = null\n      audioRecorderService.on('volume-update', (v: number) => {\n        volume = v\n      })\n\n      const emptyMessage = Buffer.concat([\n        Buffer.from([2]), // MSG_TYPE_AUDIO\n        Buffer.from([0, 0, 0, 0]), // Length: 0\n        Buffer.alloc(0),\n      ])\n\n      mockChildProcess.stdout.emit('data', emptyMessage)\n      await waitForProcessing()\n\n      expect(volume!).toBe(0)\n    })\n\n    test('should handle very small audio buffers', async () => {\n      let volume: number | null = null\n      audioRecorderService.on('volume-update', (v: number) => {\n        volume = v\n      })\n\n      // Test 1-byte buffer (too small for 16-bit sample)\n      const tinyMessage = Buffer.concat([\n        Buffer.from([2]), // MSG_TYPE_AUDIO\n        Buffer.from([1, 0, 0, 0]), // Length: 1\n        Buffer.from([0]), // Single byte\n      ])\n\n      mockChildProcess.stdout.emit('data', tinyMessage)\n      await waitForProcessing()\n\n      expect(volume!).toBe(0)\n    })\n\n    test('should handle odd-length audio buffers', async () => {\n      let volume: number | null = null\n      audioRecorderService.on('volume-update', (v: number) => {\n        volume = v\n      })\n\n      // Test 3-byte buffer (odd length, should handle gracefully)\n      const oddLengthAudio = Buffer.from([0, 0, 0]) // 3 bytes\n      const oddMessage = Buffer.concat([\n        Buffer.from([2]), // MSG_TYPE_AUDIO\n        Buffer.from([3, 0, 0, 0]), // Length: 3\n        oddLengthAudio,\n      ])\n\n      mockChildProcess.stdout.emit('data', oddMessage)\n      await waitForProcessing()\n\n      // Should calculate volume from the first 2 bytes (first sample)\n      expect(volume!).toBe(0)\n    })\n  })\n})\n"
  },
  {
    "path": "lib/media/audio.ts",
    "content": "import { spawn, ChildProcessWithoutNullStreams } from 'child_process'\nimport log from 'electron-log'\nimport { EventEmitter } from 'events'\nimport { getNativeBinaryPath } from './native-interface'\n\n// Message types from the native binary\nconst MSG_TYPE_JSON = 1\nconst MSG_TYPE_AUDIO = 2\n\ninterface Message {\n  type: 'json' | 'audio'\n  payload: Buffer\n}\n\nclass AudioRecorderService extends EventEmitter {\n  #audioRecorderProcess: ChildProcessWithoutNullStreams | null = null\n  #audioBuffer = Buffer.alloc(0)\n  #deviceListPromise: {\n    resolve: (value: string[]) => void\n    reject: (reason?: any) => void\n  } | null = null\n  #drainPromise: {\n    resolve: () => void\n    reject: (reason?: any) => void\n  } | null = null\n\n  constructor() {\n    super()\n  }\n\n  /**\n   * Spawns and initializes the native audio-recorder process.\n   */\n  public initialize(): void {\n    if (this.#audioRecorderProcess) {\n      log.warn('[AudioService] Audio recorder already running.')\n      return\n    }\n\n    const binaryPath = getNativeBinaryPath('audio-recorder')\n    if (!binaryPath) {\n      log.error(\n        '[AudioService] Could not determine audio recorder binary path.',\n      )\n      // Optionally emit an error event\n      this.emit('error', new Error('Audio recorder binary not found.'))\n      return\n    }\n\n    console.log(`[AudioService] Spawning audio recorder at: ${binaryPath}`)\n    try {\n      this.#audioRecorderProcess = spawn(binaryPath, [], {\n        stdio: ['pipe', 'pipe', 'pipe'],\n      })\n\n      this.#audioRecorderProcess.stdout.on('data', this.#onData.bind(this))\n      this.#audioRecorderProcess.stderr.on('data', this.#onStdErr.bind(this))\n      this.#audioRecorderProcess.on('close', this.#onClose.bind(this))\n      this.#audioRecorderProcess.on('error', this.#onError.bind(this))\n\n      this.emit('started')\n    } catch (err) {\n      log.error(\n        '[AudioService] Caught an error while spawning audio recorder:',\n        err,\n      )\n      this.#audioRecorderProcess = null\n      this.emit('error', err)\n    }\n  }\n\n  /**\n   * Stops the native audio-recorder process.\n   */\n  public terminate(): void {\n    if (this.#audioRecorderProcess) {\n      console.log('[AudioService] Stopping audio recorder process.')\n      this.#audioRecorderProcess.kill()\n      this.#audioRecorderProcess = null\n      this.emit('stopped')\n    }\n  }\n\n  /**\n   * Sends a command to start recording from a specific device.\n   */\n  public startRecording(deviceName: string): void {\n    this.#sendCommand({ command: 'start', device_name: deviceName })\n    console.log(`[AudioService] Recording started on device: ${deviceName}`)\n  }\n\n  /**\n   * Sends a command to stop the current recording.\n   */\n  public stopRecording(): void {\n    this.#sendCommand({ command: 'stop' })\n    console.log('[AudioService] Recording stopped')\n  }\n\n  /**\n   * Requests a list of available audio devices from the native process.\n   */\n  public getDeviceList(): Promise<string[]> {\n    return new Promise((resolve, reject) => {\n      if (!this.#audioRecorderProcess) {\n        return reject(new Error('Audio recorder process not running.'))\n      }\n      this.#deviceListPromise = { resolve, reject }\n      this.#sendCommand({ command: 'list-devices' })\n    })\n  }\n\n  /**\n   * Requests the effective output audio configuration (sample rate, channels)\n   * that the recorder will use for a given device. Resolves via 'audio-config'.\n   */\n  public requestDeviceConfig(deviceName: string): void {\n    this.#sendCommand({ command: 'get-device-config', device_name: deviceName })\n  }\n\n  // --- Private Methods ---\n\n  /**\n   * Handles incoming data chunks from the process's stdout.\n   */\n  #onData(chunk: Buffer): void {\n    this.#audioBuffer = Buffer.concat([this.#audioBuffer, chunk])\n    this.#processData()\n  }\n\n  #onStdErr(data: Buffer): void {\n    log.error('[AudioService] stderr:', data.toString())\n  }\n\n  #onClose(code: number | null): void {\n    log.warn(`[AudioService] Process exited with code: ${code}`)\n    this.#audioRecorderProcess = null\n    this.emit('stopped')\n  }\n\n  #onError(err: Error): void {\n    log.error('[AudioService] Failed to start audio recorder:', err)\n    this.#audioRecorderProcess = null\n    this.emit('error', err)\n  }\n\n  /**\n   * Parses the internal buffer for complete messages and processes them.\n   * This function is now cleaner, acting as a loop that calls helper methods.\n   */\n  #processData(): void {\n    while (true) {\n      const message = this.#parseMessage()\n      if (!message) {\n        break // Not enough data for a full message, wait for more.\n      }\n      this.#handleMessage(message)\n    }\n  }\n\n  /**\n   * Tries to parse a single message from the buffer.\n   * If a full message is available, it returns the message and slices the buffer.\n   * Otherwise, it returns null.\n   */\n  #parseMessage(): Message | null {\n    if (this.#audioBuffer.length < 5) return null // 1 byte type + 4 bytes length\n\n    const msgType = this.#audioBuffer.readUInt8(0)\n    const msgLen = this.#audioBuffer.readUInt32LE(1)\n    const frameLen = 5 + msgLen\n\n    if (this.#audioBuffer.length < frameLen) return null // Incomplete frame\n\n    const payload = this.#audioBuffer.slice(5, frameLen)\n    this.#audioBuffer = this.#audioBuffer.slice(frameLen) // Consume the message from the buffer\n\n    switch (msgType) {\n      case MSG_TYPE_JSON:\n        return { type: 'json', payload }\n      case MSG_TYPE_AUDIO:\n        return { type: 'audio', payload }\n      default:\n        log.warn(`[AudioService] Unknown message type: ${msgType}`)\n        return null // Or handle error appropriately\n    }\n  }\n\n  /**\n   * Handles a parsed message by emitting corresponding events.\n   * This completely removes side effects from the data processing logic.\n   */\n  #handleMessage(message: Message): void {\n    if (message.type === 'json') {\n      try {\n        const jsonResponse = JSON.parse(message.payload.toString('utf-8'))\n        if (jsonResponse.type === 'device-list' && this.#deviceListPromise) {\n          this.#deviceListPromise.resolve(jsonResponse.devices || [])\n          this.#deviceListPromise = null\n        } else if (jsonResponse.type === 'audio-config') {\n          const inputRate = Number(jsonResponse.input_sample_rate) || 16000\n          const outputRate = Number(jsonResponse.output_sample_rate) || 16000\n          const channels = Number(jsonResponse.channels) || 1\n          this.emit('audio-config', {\n            sampleRate: inputRate,\n            outputSampleRate: outputRate,\n            channels,\n          })\n        } else if (jsonResponse.type === 'drain-complete') {\n          if (this.#drainPromise) {\n            this.#drainPromise.resolve()\n            this.#drainPromise = null\n          }\n        }\n        // You could emit a generic 'json-message' event here if needed\n      } catch (err) {\n        log.error('[AudioService] Failed to parse JSON response:', err)\n        // Optionally reject pending device list promise if parsing fails\n        if (this.#deviceListPromise) {\n          this.#deviceListPromise.reject(\n            new Error('Failed to parse JSON response'),\n          )\n          this.#deviceListPromise = null\n        }\n        if (this.#drainPromise) {\n          this.#drainPromise.reject(err as Error)\n          this.#drainPromise = null\n        }\n      }\n    } else if (message.type === 'audio') {\n      const volume = this.#calculateVolume(message.payload)\n\n      this.emit('volume-update', volume)\n      this.emit('audio-chunk', message.payload)\n    }\n  }\n\n  public awaitDrainComplete(timeoutMs: number = 500): Promise<void> {\n    if (this.#drainPromise) {\n      return new Promise((resolve, reject) => {\n        this.once('error', reject)\n        this.#drainPromise = { resolve, reject }\n      })\n    }\n    return new Promise((resolve, reject) => {\n      let settled = false\n      const onTimeout = setTimeout(() => {\n        if (!settled) {\n          settled = true\n          this.#drainPromise = null\n          resolve() // fallback: do not hang the stop flow\n        }\n      }, timeoutMs)\n      this.#drainPromise = {\n        resolve: () => {\n          if (!settled) {\n            settled = true\n            clearTimeout(onTimeout)\n            resolve()\n          }\n        },\n        reject: (err?: any) => {\n          if (!settled) {\n            settled = true\n            clearTimeout(onTimeout)\n            reject(err)\n          }\n        },\n      }\n    })\n  }\n\n  #sendCommand(command: object): void {\n    if (this.#audioRecorderProcess?.stdin) {\n      const cmdString = JSON.stringify(command) + '\\n'\n      this.#audioRecorderProcess.stdin.write(cmdString)\n    } else {\n      log.warn('[AudioService] Cannot send command, process not running.')\n    }\n  }\n\n  #calculateVolume(buffer: Buffer): number {\n    if (buffer.length < 2) return 0\n    let sumOfSquares = 0\n    for (let i = 0; i < buffer.length - 1; i += 2) {\n      const sample = buffer.readInt16LE(i)\n      sumOfSquares += sample * sample\n    }\n    const rms = Math.sqrt(sumOfSquares / (buffer.length / 2))\n    return Math.min(rms / 32767, 1.0)\n  }\n}\n\n// Export a singleton instance of the service\nexport const audioRecorderService = new AudioRecorderService()\n"
  },
  {
    "path": "lib/media/keyboard.test.ts",
    "content": "import { ItoMode } from '@/app/generated/ito_pb'\nimport { describe, test, expect, beforeEach, mock } from 'bun:test'\nimport { EventEmitter } from 'events'\nimport { fakeTimers } from '../__tests__/helpers/testUtils'\nimport { createMockTimingCollector } from '../__tests__/setup'\n\nconst clock = fakeTimers()\n\n// Mock all external dependencies\nconst mockChildProcess = {\n  stdin: {\n    write: mock(),\n  },\n  stdout: new EventEmitter(),\n  stderr: new EventEmitter(),\n  on: mock((event: string, handler: any) => {\n    // Store handlers so tests can verify they were registered\n    if (event === 'close') {\n      mockChildProcess._closeHandler = handler as (\n        code: number,\n        signal: string,\n      ) => void\n    } else if (event === 'error') {\n      mockChildProcess._errorHandler = handler as (err: Error) => void\n    }\n  }),\n  kill: mock(),\n  unref: mock(),\n  pid: 12345,\n  _closeHandler: null as ((code: number, signal: string) => void) | null,\n  _errorHandler: null as ((err: Error) => void) | null,\n}\n\nconst mockSpawn = mock(() => mockChildProcess)\n\nmock.module('child_process', () => ({\n  spawn: mockSpawn,\n}))\n// Some environments resolve to node:child_process; mock that as well\nmock.module('node:child_process', () => ({\n  spawn: mockSpawn,\n}))\n\nconst mockMainStore = {\n  get: mock(() => ({\n    isShortcutGloballyEnabled: true,\n    keyboardShortcuts: [\n      {\n        id: 'mock-shortcut-1',\n        keys: ['command', 'space'],\n        mode: ItoMode.TRANSCRIBE,\n      },\n    ],\n  })),\n}\nmock.module('../main/store', () => ({\n  default: mockMainStore,\n}))\n\nmock.module('../constants/store-keys', () => ({\n  STORE_KEYS: {\n    SETTINGS: 'settings',\n  },\n}))\n\nconst mockGetNativeBinaryPath = mock(() => '/path/to/global-key-listener')\nmock.module('./native-interface', () => ({\n  getNativeBinaryPath: mockGetNativeBinaryPath,\n}))\n\n// Create a consistent window mock that will be reused\nconst mockWindow = {\n  webContents: {\n    send: mock(),\n    isDestroyed: mock(() => false),\n  },\n}\n\nconst mockBrowserWindow = {\n  getAllWindows: mock(() => [mockWindow]),\n}\nmock.module('electron', () => ({\n  BrowserWindow: mockBrowserWindow,\n}))\n\nconst mockAudioRecorderService = {\n  stopRecording: mock(),\n}\nmock.module('./audio', () => ({\n  audioRecorderService: mockAudioRecorderService,\n}))\n\nconst mockitoSessionManager = {\n  startSession: mock(),\n  completeSession: mock(),\n  setMode: mock(),\n  cancelSession: mock(),\n}\nmock.module('../main/itoSessionManager', () => ({\n  itoSessionManager: mockitoSessionManager,\n}))\n\nconst mockTimingCollector = createMockTimingCollector()\nmock.module('../main/timing/TimingCollector', () => ({\n  timingCollector: mockTimingCollector,\n}))\n\nconst mockInteractionManager = {\n  getCurrentInteractionId: mock(() => 'test-interaction-123'),\n  initialize: mock(() => 'test-interaction-123'),\n}\nmock.module('../main/interactions/InteractionManager', () => ({\n  interactionManager: mockInteractionManager,\n}))\n\n// Mock console to avoid spam\nbeforeEach(async () => {\n  console.log = mock()\n  console.info = mock()\n  console.warn = mock()\n  console.error = mock()\n})\n\ndescribe('Keyboard Module', () => {\n  beforeEach(async () => {\n    // Reset all mocks\n    mockSpawn.mockClear()\n    mockChildProcess.stdin.write.mockClear()\n    mockChildProcess.on.mockClear()\n    mockChildProcess.kill.mockClear()\n    mockChildProcess.unref.mockClear()\n    mockMainStore.get.mockClear()\n    mockGetNativeBinaryPath.mockClear()\n    mockBrowserWindow.getAllWindows.mockClear()\n    mockWindow.webContents.send.mockClear()\n    mockWindow.webContents.isDestroyed.mockClear()\n    mockAudioRecorderService.stopRecording.mockClear()\n    mockitoSessionManager.startSession.mockClear()\n    mockitoSessionManager.completeSession.mockClear()\n    mockitoSessionManager.setMode.mockClear()\n    mockitoSessionManager.cancelSession.mockClear()\n    Object.values(mockInteractionManager).forEach(mockFn => mockFn.mockClear())\n    Object.values(mockTimingCollector).forEach(mockFn => {\n      if (typeof mockFn === 'function' && 'mockClear' in mockFn) {\n        mockFn.mockClear()\n      }\n    })\n\n    // Reset default behaviors\n    mockInteractionManager.getCurrentInteractionId.mockReturnValue(\n      'test-interaction-123',\n    )\n    mockInteractionManager.initialize.mockReturnValue('test-interaction-123')\n\n    // Reset child process to clean state\n    mockChildProcess.stdout.removeAllListeners()\n    mockChildProcess.stderr.removeAllListeners()\n    mockChildProcess._closeHandler = null\n    mockChildProcess._errorHandler = null\n\n    // Ensure mockSpawn returns the mock process\n    mockSpawn.mockReturnValue(mockChildProcess)\n\n    // Reset module state using the resetForTesting function\n    const keyboardModule = await import('./keyboard')\n    keyboardModule.resetForTesting()\n\n    // Reset mock window to clean state\n    mockWindow.webContents.isDestroyed.mockReturnValue(false)\n\n    // Set default mock return values\n    mockMainStore.get.mockReturnValue({\n      isShortcutGloballyEnabled: true,\n      keyboardShortcuts: [\n        {\n          id: 'mock-shortcut-1',\n          keys: ['command', 'space'],\n          mode: ItoMode.TRANSCRIBE,\n        },\n      ],\n    })\n    mockGetNativeBinaryPath.mockReturnValue('/path/to/global-key-listener')\n  })\n\n  describe('Process Management Business Logic', () => {\n    test('should prevent multiple key listener instances', async () => {\n      const { startKeyListener } = await import('./keyboard')\n\n      // Start first instance\n      startKeyListener()\n      mockSpawn.mockClear()\n\n      // Try to start second instance\n      startKeyListener()\n\n      expect(mockSpawn).not.toHaveBeenCalled()\n      expect(console.warn).toHaveBeenCalledWith('Key listener already running.')\n    })\n\n    test('should handle missing binary path gracefully', async () => {\n      mockGetNativeBinaryPath.mockReturnValue('')\n      const { startKeyListener } = await import('./keyboard')\n\n      startKeyListener()\n\n      expect(mockSpawn).not.toHaveBeenCalled()\n      expect(console.error).toHaveBeenCalledWith(\n        'Could not determine key listener binary path.',\n      )\n    })\n\n    test('should handle spawn errors gracefully', async () => {\n      const spawnError = new Error('Failed to spawn process')\n      mockSpawn.mockImplementation(() => {\n        throw spawnError\n      })\n      const { startKeyListener } = await import('./keyboard')\n\n      startKeyListener()\n\n      expect(console.error).toHaveBeenCalledWith(\n        'Failed to start key listener:',\n        spawnError,\n      )\n    })\n  })\n\n  describe('Message Parsing Business Logic', () => {\n    test('should handle fragmented JSON from stdout', async () => {\n      const { startKeyListener } = await import('./keyboard')\n\n      startKeyListener()\n\n      const keyEvent = {\n        type: 'keydown',\n        key: 'Space',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 32,\n      }\n\n      const jsonString = JSON.stringify(keyEvent) + '\\n'\n      const fragment1 = jsonString.slice(0, 20)\n      const fragment2 = jsonString.slice(20)\n\n      // Send fragmented data\n      mockChildProcess.stdout.emit('data', Buffer.from(fragment1))\n      mockChildProcess.stdout.emit('data', Buffer.from(fragment2))\n\n      // Should still process the complete event\n      expect(mockWindow.webContents.send).toHaveBeenCalledWith(\n        'key-event',\n        keyEvent,\n      )\n    })\n\n    test('should handle multiple events in single data chunk', async () => {\n      const { startKeyListener } = await import('./keyboard')\n\n      startKeyListener()\n\n      const event1 = {\n        type: 'keydown',\n        key: 'KeyA',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 65,\n      }\n      const event2 = {\n        type: 'keyup',\n        key: 'KeyA',\n        timestamp: '2024-01-01T00:00:00.001Z',\n        raw_code: 65,\n      }\n\n      const combinedData =\n        JSON.stringify(event1) + '\\n' + JSON.stringify(event2) + '\\n'\n      mockChildProcess.stdout.emit('data', Buffer.from(combinedData))\n\n      // Should process both events\n      expect(mockWindow.webContents.send).toHaveBeenCalledTimes(2)\n      expect(mockWindow.webContents.send).toHaveBeenCalledWith(\n        'key-event',\n        event1,\n      )\n      expect(mockWindow.webContents.send).toHaveBeenCalledWith(\n        'key-event',\n        event2,\n      )\n    })\n\n    test('should handle malformed JSON gracefully', async () => {\n      const { startKeyListener } = await import('./keyboard')\n\n      startKeyListener()\n\n      const malformedJson = '{\"type\": \"keydown\", \"key\":\\n'\n      mockChildProcess.stdout.emit('data', Buffer.from(malformedJson))\n\n      expect(console.error).toHaveBeenCalledWith(\n        'Failed to parse key process event:',\n        malformedJson.trim(),\n        expect.any(Error),\n      )\n    })\n  })\n\n  describe('Window Event Broadcasting Business Logic', () => {\n    test('should broadcast events to all non-destroyed windows', async () => {\n      const { startKeyListener } = await import('./keyboard')\n\n      // Create multiple windows\n      const window1 = {\n        webContents: {\n          send: mock(),\n          isDestroyed: mock(() => false),\n        },\n      }\n      const window2 = {\n        webContents: {\n          send: mock(),\n          isDestroyed: mock(() => false),\n        },\n      }\n      mockBrowserWindow.getAllWindows.mockReturnValue([window1, window2])\n\n      startKeyListener()\n\n      const keyEvent = {\n        type: 'keydown',\n        key: 'KeyA',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 65,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(keyEvent) + '\\n'),\n      )\n\n      // Should send to both windows\n      expect(window1.webContents.send).toHaveBeenCalledWith(\n        'key-event',\n        keyEvent,\n      )\n      expect(window2.webContents.send).toHaveBeenCalledWith(\n        'key-event',\n        keyEvent,\n      )\n    })\n\n    test('should skip destroyed windows when broadcasting events', async () => {\n      const { startKeyListener } = await import('./keyboard')\n\n      // Create windows with one destroyed\n      const window1 = {\n        webContents: {\n          send: mock(),\n          isDestroyed: mock(() => false),\n        },\n      }\n      const destroyedWindow = {\n        webContents: {\n          send: mock(),\n          isDestroyed: mock(() => true),\n        },\n      }\n      mockBrowserWindow.getAllWindows.mockReturnValue([\n        window1,\n        destroyedWindow,\n      ])\n\n      startKeyListener()\n\n      const keyEvent = {\n        type: 'keydown',\n        key: 'KeyA',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 65,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(keyEvent) + '\\n'),\n      )\n\n      // Should only send to non-destroyed window\n      expect(window1.webContents.send).toHaveBeenCalledWith(\n        'key-event',\n        keyEvent,\n      )\n      expect(destroyedWindow.webContents.send).not.toHaveBeenCalled()\n    })\n  })\n\n  describe('Shortcut Detection Business Logic', () => {\n    test('should activate shortcut when keys match', async () => {\n      mockMainStore.get.mockReturnValue({\n        isShortcutGloballyEnabled: true,\n        keyboardShortcuts: [\n          {\n            id: 'test-shortcut-1',\n            keys: ['command', 'space'],\n            mode: ItoMode.TRANSCRIBE,\n          },\n        ],\n      })\n\n      const { startKeyListener } = await import('./keyboard')\n      startKeyListener()\n\n      // Press command key\n      const commandDown = {\n        type: 'keydown',\n        key: 'MetaLeft',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 91,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(commandDown) + '\\n'),\n      )\n\n      // Press space key\n      const spaceDown = {\n        type: 'keydown',\n        key: 'Space',\n        timestamp: '2024-01-01T00:00:00.001Z',\n        raw_code: 32,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(spaceDown) + '\\n'),\n      )\n\n      expect(mockitoSessionManager.startSession).toHaveBeenCalled()\n      expect(console.info).toHaveBeenCalledWith(\n        'lib Shortcut ACTIVATED, starting recording...',\n      )\n    })\n\n    test('should deactivate shortcut when keys are released', async () => {\n      mockMainStore.get.mockReturnValue({\n        isShortcutGloballyEnabled: true,\n        keyboardShortcuts: [\n          {\n            id: 'test-shortcut',\n            keys: ['command', 'space'],\n            mode: ItoMode.TRANSCRIBE,\n          },\n        ],\n      })\n\n      const { startKeyListener } = await import('./keyboard')\n      startKeyListener()\n\n      // Activate shortcut first\n      const commandDown = {\n        type: 'keydown',\n        key: 'MetaLeft',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 91,\n      }\n      const spaceDown = {\n        type: 'keydown',\n        key: 'Space',\n        timestamp: '2024-01-01T00:00:00.001Z',\n        raw_code: 32,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(commandDown) + '\\n'),\n      )\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(spaceDown) + '\\n'),\n      )\n\n      // Release space key\n      const spaceUp = {\n        type: 'keyup',\n        key: 'Space',\n        timestamp: '2024-01-01T00:00:00.002Z',\n        raw_code: 32,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(spaceUp) + '\\n'),\n      )\n\n      expect(mockitoSessionManager.completeSession).toHaveBeenCalled()\n      expect(console.info).toHaveBeenCalledWith(\n        'lib Shortcut DEACTIVATED, stopping recording...',\n      )\n    })\n\n    test('should not activate shortcut when globally disabled', async () => {\n      mockMainStore.get.mockReturnValue({\n        isShortcutGloballyEnabled: false,\n        keyboardShortcuts: [\n          {\n            id: 'test-shortcut',\n            keys: ['command', 'space'],\n            mode: ItoMode.TRANSCRIBE,\n          },\n        ],\n      })\n\n      const { startKeyListener } = await import('./keyboard')\n      startKeyListener()\n\n      const commandDown = {\n        type: 'keydown',\n        key: 'MetaLeft',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 91,\n      }\n      const spaceDown = {\n        type: 'keydown',\n        key: 'Space',\n        timestamp: '2024-01-01T00:00:00.001Z',\n        raw_code: 32,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(commandDown) + '\\n'),\n      )\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(spaceDown) + '\\n'),\n      )\n\n      expect(mockitoSessionManager.startSession).not.toHaveBeenCalled()\n    })\n\n    test('should stop active recording when shortcut is disabled', async () => {\n      let isShortcutGloballyEnabled = true\n      mockMainStore.get.mockImplementation(() => ({\n        isShortcutGloballyEnabled,\n        keyboardShortcuts: [\n          {\n            id: 'disable-test',\n            keys: ['command', 'space'],\n            mode: ItoMode.TRANSCRIBE,\n          },\n        ],\n      }))\n\n      const { startKeyListener } = await import('./keyboard')\n      startKeyListener()\n\n      // Activate shortcut\n      const commandDown = {\n        type: 'keydown',\n        key: 'MetaLeft',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 91,\n      }\n      const spaceDown = {\n        type: 'keydown',\n        key: 'Space',\n        timestamp: '2024-01-01T00:00:00.001Z',\n        raw_code: 32,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(commandDown) + '\\n'),\n      )\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(spaceDown) + '\\n'),\n      )\n\n      // Disable shortcuts\n      isShortcutGloballyEnabled = false\n\n      // Send another key event to trigger check\n      const otherKey = {\n        type: 'keydown',\n        key: 'KeyA',\n        timestamp: '2024-01-01T00:00:00.002Z',\n        raw_code: 65,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(otherKey) + '\\n'),\n      )\n\n      expect(mockitoSessionManager.completeSession).toHaveBeenCalled()\n      expect(console.info).toHaveBeenCalledWith(\n        'Shortcut DEACTIVATED, stopping recording...',\n      )\n    })\n\n    test('should ignore fast fn key events', async () => {\n      // Create fresh mock objects for this test to avoid isolation issues\n      const freshMockWindow = {\n        webContents: {\n          send: mock(),\n          isDestroyed: mock(() => false),\n        },\n      }\n\n      const freshMockBrowserWindow = {\n        getAllWindows: mock(() => [freshMockWindow]),\n      }\n\n      // Temporarily override the electron mock for this test\n      const originalGetAllWindows = mockBrowserWindow.getAllWindows\n      mockBrowserWindow.getAllWindows = freshMockBrowserWindow.getAllWindows\n\n      try {\n        const { startKeyListener } = await import('./keyboard')\n        startKeyListener()\n\n        const fastFnEvent = {\n          type: 'keydown',\n          key: 'Unknown(179)',\n          timestamp: '2024-01-01T00:00:00.000Z',\n          raw_code: 179,\n        }\n        mockChildProcess.stdout.emit(\n          'data',\n          Buffer.from(JSON.stringify(fastFnEvent) + '\\n'),\n        )\n\n        // Should still forward to windows but not affect shortcut state\n        expect(freshMockWindow.webContents.send).toHaveBeenCalledWith(\n          'key-event',\n          fastFnEvent,\n        )\n      } finally {\n        // Restore original mock\n        mockBrowserWindow.getAllWindows = originalGetAllWindows\n      }\n    })\n\n    test('should handle complex multi-key shortcuts', async () => {\n      mockMainStore.get.mockReturnValue({\n        isShortcutGloballyEnabled: true,\n        keyboardShortcuts: [\n          {\n            id: 'complex-shortcut',\n            keys: ['control', 'shift', 'f'],\n            mode: ItoMode.TRANSCRIBE,\n          },\n        ],\n      })\n\n      const { startKeyListener } = await import('./keyboard')\n      startKeyListener()\n\n      // Press all keys in sequence\n      const controlDown = {\n        type: 'keydown',\n        key: 'ControlLeft',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 17,\n      }\n      const shiftDown = {\n        type: 'keydown',\n        key: 'ShiftLeft',\n        timestamp: '2024-01-01T00:00:00.001Z',\n        raw_code: 16,\n      }\n      const fDown = {\n        type: 'keydown',\n        key: 'KeyF',\n        timestamp: '2024-01-01T00:00:00.002Z',\n        raw_code: 70,\n      }\n\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(controlDown) + '\\n'),\n      )\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(shiftDown) + '\\n'),\n      )\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(fDown) + '\\n'),\n      )\n\n      expect(mockitoSessionManager.startSession).toHaveBeenCalled()\n    })\n\n    test('should handle partial shortcut matches correctly', async () => {\n      mockMainStore.get.mockReturnValue({\n        isShortcutGloballyEnabled: true,\n        keyboardShortcuts: [\n          {\n            id: 'partial-test',\n            keys: ['command', 'shift', 'a'],\n            mode: ItoMode.TRANSCRIBE,\n          },\n        ],\n      })\n\n      const { startKeyListener } = await import('./keyboard')\n      startKeyListener()\n\n      // Press only command and shift (partial match)\n      const commandDown = {\n        type: 'keydown',\n        key: 'MetaLeft',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 91,\n      }\n      const shiftDown = {\n        type: 'keydown',\n        key: 'ShiftLeft',\n        timestamp: '2024-01-01T00:00:00.001Z',\n        raw_code: 16,\n      }\n\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(commandDown) + '\\n'),\n      )\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(shiftDown) + '\\n'),\n      )\n\n      // Should not activate shortcut with partial match\n      expect(mockitoSessionManager.startSession).not.toHaveBeenCalled()\n    })\n\n    test('should not activate shortcut when superset of keys is pressed', async () => {\n      mockMainStore.get.mockReturnValue({\n        isShortcutGloballyEnabled: true,\n        keyboardShortcuts: [\n          {\n            id: 'superset-test',\n            keys: ['fn'],\n            mode: ItoMode.TRANSCRIBE,\n          },\n        ],\n      })\n\n      const { startKeyListener } = await import('./keyboard')\n      startKeyListener()\n\n      // Press control first, then fn (so fn+control are pressed together but shortcut should not match)\n      const controlDown = {\n        type: 'keydown',\n        key: 'ControlLeft',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 17,\n      }\n      const fnDown = {\n        type: 'keydown',\n        key: 'Function',\n        timestamp: '2024-01-01T00:00:00.001Z',\n        raw_code: 179,\n      }\n\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(controlDown) + '\\n'),\n      )\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(fnDown) + '\\n'),\n      )\n\n      // Should not activate shortcut when superset is pressed\n      expect(mockitoSessionManager.startSession).not.toHaveBeenCalled()\n    })\n\n    test('should require exact key match for shortcut activation', async () => {\n      mockMainStore.get.mockReturnValue({\n        isShortcutGloballyEnabled: true,\n        keyboardShortcuts: [\n          {\n            id: 'exact-match-test',\n            keys: ['fn'],\n            mode: ItoMode.TRANSCRIBE,\n          },\n        ],\n      })\n\n      const { startKeyListener } = await import('./keyboard')\n      startKeyListener()\n\n      // Press exactly fn (exact match)\n      const fnDown = {\n        type: 'keydown',\n        key: 'Function',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 179,\n      }\n\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(fnDown) + '\\n'),\n      )\n\n      // Should activate shortcut with exact match\n      expect(mockitoSessionManager.startSession).toHaveBeenCalledWith(\n        ItoMode.TRANSCRIBE,\n      )\n    })\n\n    test('should not match when extra keys are held with configured shortcut', async () => {\n      mockMainStore.get.mockReturnValue({\n        isShortcutGloballyEnabled: true,\n        keyboardShortcuts: [\n          {\n            id: 'extra-keys-test',\n            keys: ['command', 'space'],\n            mode: ItoMode.TRANSCRIBE,\n          },\n        ],\n      })\n\n      const { startKeyListener } = await import('./keyboard')\n      startKeyListener()\n\n      // Press shift first, then command + space (so all three are pressed but shortcut should not match)\n      const shiftDown = {\n        type: 'keydown',\n        key: 'ShiftLeft',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 16,\n      }\n      const commandDown = {\n        type: 'keydown',\n        key: 'MetaLeft',\n        timestamp: '2024-01-01T00:00:00.001Z',\n        raw_code: 91,\n      }\n      const spaceDown = {\n        type: 'keydown',\n        key: 'Space',\n        timestamp: '2024-01-01T00:00:00.002Z',\n        raw_code: 32,\n      }\n\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(shiftDown) + '\\n'),\n      )\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(commandDown) + '\\n'),\n      )\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(spaceDown) + '\\n'),\n      )\n\n      // Should not activate shortcut when extra keys are pressed\n      expect(mockitoSessionManager.startSession).not.toHaveBeenCalled()\n    })\n\n    test('should allow repeated shortcut activations with exact matching', async () => {\n      mockMainStore.get.mockReturnValue({\n        isShortcutGloballyEnabled: true,\n        keyboardShortcuts: [\n          {\n            id: 'repeat-test',\n            keys: ['command', 'space'],\n            mode: ItoMode.TRANSCRIBE,\n          },\n        ],\n      })\n\n      const { startKeyListener } = await import('./keyboard')\n      startKeyListener()\n\n      // First activation cycle\n      const commandDown1 = {\n        type: 'keydown',\n        key: 'MetaLeft',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 91,\n      }\n      const spaceDown1 = {\n        type: 'keydown',\n        key: 'Space',\n        timestamp: '2024-01-01T00:00:00.001Z',\n        raw_code: 32,\n      }\n      const commandUp1 = {\n        type: 'keyup',\n        key: 'MetaLeft',\n        timestamp: '2024-01-01T00:00:00.002Z',\n        raw_code: 91,\n      }\n      const spaceUp1 = {\n        type: 'keyup',\n        key: 'Space',\n        timestamp: '2024-01-01T00:00:00.003Z',\n        raw_code: 32,\n      }\n\n      // Press command + space\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(commandDown1) + '\\n'),\n      )\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(spaceDown1) + '\\n'),\n      )\n\n      expect(mockitoSessionManager.startSession).toHaveBeenCalledTimes(1)\n\n      // Release command + space\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(commandUp1) + '\\n'),\n      )\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(spaceUp1) + '\\n'),\n      )\n\n      expect(mockitoSessionManager.completeSession).toHaveBeenCalledTimes(1)\n\n      // Clear mocks for second cycle\n      mockitoSessionManager.startSession.mockClear()\n      mockitoSessionManager.completeSession.mockClear()\n\n      // Second activation cycle - should work again\n      const commandDown2 = {\n        type: 'keydown',\n        key: 'MetaLeft',\n        timestamp: '2024-01-01T00:00:01.000Z',\n        raw_code: 91,\n      }\n      const spaceDown2 = {\n        type: 'keydown',\n        key: 'Space',\n        timestamp: '2024-01-01T00:00:01.001Z',\n        raw_code: 32,\n      }\n\n      // Press command + space again\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(commandDown2) + '\\n'),\n      )\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(spaceDown2) + '\\n'),\n      )\n\n      // Should activate shortcut again\n      expect(mockitoSessionManager.startSession).toHaveBeenCalledTimes(1)\n    })\n  })\n\n  describe('Key Normalization Business Logic', () => {\n    test('should normalize legacy modifier keys to left variants', async () => {\n      mockMainStore.get.mockReturnValue({\n        isShortcutGloballyEnabled: true,\n        keyboardShortcuts: [\n          {\n            id: 'command-test',\n            keys: ['command'], // Legacy key, should normalize to command-left\n            mode: ItoMode.TRANSCRIBE,\n          },\n        ],\n      })\n\n      const { startKeyListener } = await import('./keyboard')\n      startKeyListener()\n\n      // MetaLeft should match because 'command' normalizes to 'command-left'\n      const metaLeftDown = {\n        type: 'keydown',\n        key: 'MetaLeft',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 91,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(metaLeftDown) + '\\n'),\n      )\n\n      expect(mockitoSessionManager.startSession).toHaveBeenCalled()\n      mockitoSessionManager.startSession.mockClear()\n\n      const metaLeftUp = {\n        type: 'keyup',\n        key: 'MetaLeft',\n        timestamp: '2024-01-01T00:00:00.001Z',\n        raw_code: 91,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(metaLeftUp) + '\\n'),\n      )\n\n      // MetaRight should NOT trigger since command normalizes to command-left only\n      const metaRightDown = {\n        type: 'keydown',\n        key: 'MetaRight',\n        timestamp: '2024-01-01T00:00:00.002Z',\n        raw_code: 92,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(metaRightDown) + '\\n'),\n      )\n\n      // Should NOT have been called again\n      expect(mockitoSessionManager.startSession).not.toHaveBeenCalled()\n    })\n\n    test('should normalize letter keys correctly', async () => {\n      mockMainStore.get.mockReturnValue({\n        isShortcutGloballyEnabled: true,\n        keyboardShortcuts: [\n          {\n            id: 'letter-test',\n            keys: ['a'],\n            mode: ItoMode.TRANSCRIBE,\n          },\n        ],\n      })\n\n      const { startKeyListener } = await import('./keyboard')\n      startKeyListener()\n\n      const keyADown = {\n        type: 'keydown',\n        key: 'KeyA',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 65,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(keyADown) + '\\n'),\n      )\n\n      expect(mockitoSessionManager.startSession).toHaveBeenCalled()\n    })\n\n    test('should normalize number keys correctly', async () => {\n      mockMainStore.get.mockReturnValue({\n        isShortcutGloballyEnabled: true,\n        keyboardShortcuts: [\n          {\n            id: 'number-test',\n            keys: ['1'],\n            mode: ItoMode.TRANSCRIBE,\n          },\n        ],\n      })\n\n      const { startKeyListener } = await import('./keyboard')\n      startKeyListener()\n\n      const digit1Down = {\n        type: 'keydown',\n        key: 'Digit1',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 49,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(digit1Down) + '\\n'),\n      )\n\n      expect(mockitoSessionManager.startSession).toHaveBeenCalled()\n    })\n\n    test('should handle unknown keys by lowercasing them', async () => {\n      mockMainStore.get.mockReturnValue({\n        isShortcutGloballyEnabled: true,\n        keyboardShortcuts: [\n          {\n            id: 'unknown-test',\n            keys: ['unknownkey'],\n            mode: ItoMode.TRANSCRIBE,\n          },\n        ],\n      })\n\n      const { startKeyListener } = await import('./keyboard')\n      startKeyListener()\n\n      const unknownKeyDown = {\n        type: 'keydown',\n        key: 'UnknownKey',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 999,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(unknownKeyDown) + '\\n'),\n      )\n\n      expect(mockitoSessionManager.startSession).toHaveBeenCalled()\n    })\n  })\n\n  describe('Hotkey Registration Business Logic', () => {\n    test('should register hotkeys on startup', async () => {\n      mockMainStore.get.mockReturnValue({\n        isShortcutGloballyEnabled: true,\n        keyboardShortcuts: [\n          {\n            id: 'test-hotkey',\n            keys: ['control', 'z'],\n            mode: ItoMode.TRANSCRIBE,\n          },\n        ],\n      })\n\n      const { startKeyListener } = await import('./keyboard')\n      startKeyListener()\n\n      // Should register hotkeys with the Rust process\n      expect(mockChildProcess.stdin.write).toHaveBeenCalledWith(\n        expect.stringContaining('\"command\":\"register_hotkeys\"'),\n      )\n      expect(mockChildProcess.stdin.write).toHaveBeenCalledWith(\n        expect.stringContaining('ControlLeft'),\n      )\n    })\n\n    test('should register all hotkeys when registerAllHotkeys is called', async () => {\n      mockMainStore.get.mockReturnValue({\n        isShortcutGloballyEnabled: true,\n        keyboardShortcuts: [\n          {\n            id: 'hotkey-1',\n            keys: ['command', 'space'],\n            mode: ItoMode.TRANSCRIBE,\n          },\n          {\n            id: 'hotkey-2',\n            keys: ['control', 'shift', 'f'],\n            mode: ItoMode.EDIT,\n          },\n        ],\n      })\n\n      const { startKeyListener, registerAllHotkeys } = await import(\n        './keyboard'\n      )\n      startKeyListener()\n\n      mockChildProcess.stdin.write.mockClear()\n      registerAllHotkeys()\n\n      const writeCall = mockChildProcess.stdin.write.mock.calls[0][0]\n      expect(writeCall).toContain('\"command\":\"register_hotkeys\"')\n      expect(writeCall).toContain('MetaLeft')\n      expect(writeCall).toContain('Space')\n      expect(writeCall).toContain('ControlLeft')\n      expect(writeCall).toContain('ShiftLeft')\n      expect(writeCall).toContain('KeyF')\n    })\n\n    test('should only register hotkeys with keys defined', async () => {\n      mockMainStore.get.mockReturnValue({\n        isShortcutGloballyEnabled: true,\n        keyboardShortcuts: [\n          {\n            id: 'empty-hotkey',\n            keys: [],\n            mode: ItoMode.TRANSCRIBE,\n          },\n          {\n            id: 'valid-hotkey',\n            keys: ['control', 'a'],\n            mode: ItoMode.EDIT,\n          },\n        ],\n      })\n\n      const { startKeyListener } = await import('./keyboard')\n      startKeyListener()\n\n      const writeCall = mockChildProcess.stdin.write.mock.calls[0][0]\n      const parsed = JSON.parse(writeCall.replace('\\n', ''))\n\n      // Should only have one hotkey (the valid one)\n      expect(parsed.hotkeys).toHaveLength(1)\n      expect(parsed.hotkeys[0].keys).toContain('ControlLeft')\n      expect(parsed.hotkeys[0].keys).toContain('KeyA')\n    })\n\n    test('should warn when trying to register hotkeys without process', async () => {\n      const { registerAllHotkeys } = await import('./keyboard')\n\n      registerAllHotkeys()\n\n      expect(console.warn).toHaveBeenCalledWith(\n        'Key listener not running, cannot register hotkeys.',\n      )\n      expect(mockChildProcess.stdin.write).not.toHaveBeenCalled()\n    })\n  })\n\n  describe('Memory Management Business Logic', () => {\n    test('should clear pressed keys state on stop', async () => {\n      const { startKeyListener, stopKeyListener } = await import('./keyboard')\n\n      startKeyListener()\n\n      // Simulate some key presses\n      const keyADown = {\n        type: 'keydown',\n        key: 'KeyA',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 65,\n      }\n      const keyBDown = {\n        type: 'keydown',\n        key: 'KeyB',\n        timestamp: '2024-01-01T00:00:00.001Z',\n        raw_code: 66,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(keyADown) + '\\n'),\n      )\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(keyBDown) + '\\n'),\n      )\n\n      stopKeyListener()\n\n      // After restart, pressed keys should be cleared\n      startKeyListener()\n\n      // The shortcut that required both A and B should not be active\n      mockMainStore.get.mockReturnValue({\n        isShortcutGloballyEnabled: true,\n        keyboardShortcuts: [\n          {\n            id: 'memory-test',\n            keys: ['a', 'b'],\n            mode: ItoMode.TRANSCRIBE,\n          },\n        ],\n      })\n\n      // Only press A again - should not trigger shortcut since B was cleared\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(keyADown) + '\\n'),\n      )\n\n      expect(mockitoSessionManager.startSession).not.toHaveBeenCalled()\n    })\n  })\n\n  describe('Stuck Key Detection', () => {\n    test('should remove keys stuck for more than 5 seconds', async () => {\n      const { startKeyListener } = await import('./keyboard')\n\n      startKeyListener()\n\n      // Press a key\n      const keyADown = {\n        type: 'keydown',\n        key: 'KeyA',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 65,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(keyADown) + '\\n'),\n      )\n\n      // Advance time by more than 5 seconds (5000ms + check interval 1000ms)\n      clock.tick(6000)\n\n      // Should warn about removing stuck key\n      expect(console.warn).toHaveBeenCalledWith(\n        expect.stringContaining('Removing stuck key: a'),\n      )\n      expect(console.warn).toHaveBeenCalledWith(\n        expect.stringContaining('(held for 6s)'),\n      )\n    })\n\n    test('should not remove stuck keys that are part of active shortcuts', async () => {\n      mockMainStore.get.mockReturnValue({\n        isShortcutGloballyEnabled: true,\n        keyboardShortcuts: [\n          {\n            id: 'stuck-key-protection-test',\n            keys: ['command', 'space'],\n            mode: ItoMode.TRANSCRIBE,\n          },\n        ],\n      })\n\n      const { startKeyListener } = await import('./keyboard')\n      startKeyListener()\n\n      // Activate shortcut\n      const commandDown = {\n        type: 'keydown',\n        key: 'MetaLeft',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 91,\n      }\n      const spaceDown = {\n        type: 'keydown',\n        key: 'Space',\n        timestamp: '2024-01-01T00:00:00.001Z',\n        raw_code: 32,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(commandDown) + '\\n'),\n      )\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(spaceDown) + '\\n'),\n      )\n\n      // Advance time by more than 5 seconds\n      clock.tick(6000)\n\n      // Should not warn about removing stuck keys since they're part of active shortcut\n      expect(console.warn).not.toHaveBeenCalledWith(\n        expect.stringContaining('Removing stuck key'),\n      )\n    })\n\n    test('should remove stuck keys that are not part of active shortcuts', async () => {\n      mockMainStore.get.mockReturnValue({\n        isShortcutGloballyEnabled: true,\n        keyboardShortcuts: [\n          {\n            id: 'partial-stuck-test',\n            keys: ['command', 'space'],\n            mode: ItoMode.TRANSCRIBE,\n          },\n        ],\n      })\n\n      const { startKeyListener } = await import('./keyboard')\n      startKeyListener()\n\n      // Activate shortcut\n      const commandDown = {\n        type: 'keydown',\n        key: 'MetaLeft',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 91,\n      }\n      const spaceDown = {\n        type: 'keydown',\n        key: 'Space',\n        timestamp: '2024-01-01T00:00:00.001Z',\n        raw_code: 32,\n      }\n      // Press an extra key that's not part of the shortcut\n      const keyADown = {\n        type: 'keydown',\n        key: 'KeyA',\n        timestamp: '2024-01-01T00:00:00.002Z',\n        raw_code: 65,\n      }\n\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(commandDown) + '\\n'),\n      )\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(spaceDown) + '\\n'),\n      )\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(keyADown) + '\\n'),\n      )\n\n      // Advance time by more than 5 seconds\n      clock.tick(6000)\n\n      // Should warn about removing the stuck key that's not part of the active shortcut\n      expect(console.warn).toHaveBeenCalledWith(\n        expect.stringContaining('Removing stuck key: a'),\n      )\n    })\n\n    test('should not check for stuck keys when no shortcut is active', async () => {\n      const { startKeyListener } = await import('./keyboard')\n\n      startKeyListener()\n\n      // Press some keys without activating any shortcuts\n      const keyADown = {\n        type: 'keydown',\n        key: 'KeyA',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 65,\n      }\n      const keyBDown = {\n        type: 'keydown',\n        key: 'KeyB',\n        timestamp: '2024-01-01T00:00:00.001Z',\n        raw_code: 66,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(keyADown) + '\\n'),\n      )\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(keyBDown) + '\\n'),\n      )\n\n      // Advance time by more than 5 seconds\n      clock.tick(6000)\n\n      // Should still remove stuck keys even when no shortcut is active\n      expect(console.warn).toHaveBeenCalledWith(\n        expect.stringContaining('Removing stuck key: a'),\n      )\n      expect(console.warn).toHaveBeenCalledWith(\n        expect.stringContaining('Removing stuck key: b'),\n      )\n    })\n\n    test('should clear stuck key tracking on key release', async () => {\n      const { startKeyListener } = await import('./keyboard')\n\n      startKeyListener()\n\n      // Press and release a key quickly\n      const keyADown = {\n        type: 'keydown',\n        key: 'KeyA',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 65,\n      }\n      const keyAUp = {\n        type: 'keyup',\n        key: 'KeyA',\n        timestamp: '2024-01-01T00:00:00.100Z',\n        raw_code: 65,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(keyADown) + '\\n'),\n      )\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(keyAUp) + '\\n'),\n      )\n\n      // Advance time by more than 5 seconds\n      clock.tick(6000)\n\n      // Should not warn about stuck key since it was released\n      expect(console.warn).not.toHaveBeenCalledWith(\n        expect.stringContaining('Removing stuck key: a'),\n      )\n    })\n\n    test('should not track duplicate keydown events for same key', async () => {\n      const { startKeyListener } = await import('./keyboard')\n\n      startKeyListener()\n\n      // Press same key multiple times (simulating key repeat)\n      const keyADown1 = {\n        type: 'keydown',\n        key: 'KeyA',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 65,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(keyADown1) + '\\n'),\n      )\n\n      // Advance time slightly\n      clock.tick(1000)\n\n      const keyADown2 = {\n        type: 'keydown',\n        key: 'KeyA',\n        timestamp: '2024-01-01T00:00:01.000Z',\n        raw_code: 65,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(keyADown2) + '\\n'),\n      )\n\n      // Advance time by more than 5 seconds from first press\n      clock.tick(5000)\n\n      // Should warn about stuck key based on first timestamp, not second\n      expect(console.warn).toHaveBeenCalledWith(\n        expect.stringContaining('Removing stuck key: a'),\n      )\n      expect(console.warn).toHaveBeenCalledWith(\n        expect.stringContaining('(held for 6s)'),\n      )\n    })\n\n    test('should clean up stuck key checker on stop', async () => {\n      const { startKeyListener, stopKeyListener } = await import('./keyboard')\n\n      startKeyListener()\n\n      // Press a key\n      const keyADown = {\n        type: 'keydown',\n        key: 'KeyA',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 65,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(keyADown) + '\\n'),\n      )\n\n      // Stop the key listener\n      stopKeyListener()\n\n      // Advance time by more than 5 seconds\n      clock.tick(6000)\n\n      // Should not warn about stuck keys since listener was stopped\n      expect(console.warn).not.toHaveBeenCalledWith(\n        expect.stringContaining('Removing stuck key'),\n      )\n    })\n\n    test('should clean up stuck key data in resetForTesting', async () => {\n      const { startKeyListener, resetForTesting } = await import('./keyboard')\n\n      startKeyListener()\n\n      // Press a key\n      const keyADown = {\n        type: 'keydown',\n        key: 'KeyA',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 65,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(keyADown) + '\\n'),\n      )\n\n      // Reset for testing\n      resetForTesting()\n\n      // Start again\n      startKeyListener()\n\n      // Advance time by more than 5 seconds\n      clock.tick(6000)\n\n      // Should not warn about stuck keys since data was reset\n      expect(console.warn).not.toHaveBeenCalledWith(\n        expect.stringContaining('Removing stuck key'),\n      )\n    })\n\n    test('should handle check interval timing correctly', async () => {\n      const { startKeyListener } = await import('./keyboard')\n\n      startKeyListener()\n\n      // Press a key\n      const keyADown = {\n        type: 'keydown',\n        key: 'KeyA',\n        timestamp: '2024-01-01T00:00:00.000Z',\n        raw_code: 65,\n      }\n      mockChildProcess.stdout.emit(\n        'data',\n        Buffer.from(JSON.stringify(keyADown) + '\\n'),\n      )\n\n      // Advance time by exactly 5 seconds (should not trigger removal yet)\n      clock.tick(5000)\n      expect(console.warn).not.toHaveBeenCalledWith(\n        expect.stringContaining('Removing stuck key'),\n      )\n\n      // Advance time by the check interval (should trigger removal)\n      clock.tick(1000)\n      expect(console.warn).toHaveBeenCalledWith(\n        expect.stringContaining('Removing stuck key: a'),\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "lib/media/keyboard.ts",
    "content": "import { spawn } from 'child_process'\nimport store, { KeyboardShortcutConfig } from '../main/store'\nimport { STORE_KEYS } from '../constants/store-keys'\nimport { getNativeBinaryPath } from './native-interface'\nimport { BrowserWindow } from 'electron'\nimport { itoSessionManager } from '../main/itoSessionManager'\nimport { KeyName, keyNameMap, normalizeLegacyKey } from '../types/keyboard'\n\ninterface KeyEvent {\n  type: 'keydown' | 'keyup'\n  key: string\n  timestamp: string\n  raw_code: number\n}\n\ninterface HeartbeatEvent {\n  type: 'heartbeat_ping'\n  id: string\n  timestamp: string\n}\n\ninterface RegisteredHotkeysEvent {\n  type: 'registered_hotkeys'\n  hotkeys: Array<{ keys: string[] }>\n}\n\ntype ProcessEvent = KeyEvent | HeartbeatEvent | RegisteredHotkeysEvent\n\n// Global key listener process singleton\nexport let KeyListenerProcess: ReturnType<typeof spawn> | null = null\nlet activeShortcutId: string | null = null\n\n// Heartbeat monitoring state\nlet lastHeartbeatReceived = Date.now()\nlet heartbeatCheckTimer: NodeJS.Timeout | null = null\nconst HEARTBEAT_CHECK_INTERVAL_MS = 5000 // Check every 5 seconds\nconst HEARTBEAT_TIMEOUT_MS = 15000 // 15 seconds without heartbeat triggers restart\n\n// Test utility function - only available in development\nexport const resetForTesting = () => {\n  if (process.env.NODE_ENV !== 'production') {\n    KeyListenerProcess = null\n    activeShortcutId = null\n    pressedKeys.clear()\n    keyPressTimestamps.clear()\n    stopStuckKeyChecker()\n    stopHeartbeatChecker()\n    lastHeartbeatReceived = Date.now()\n  }\n}\n\nconst nativeModuleName = 'global-key-listener'\n\n// Normalizes a raw key event into a consistent string\nfunction normalizeKey(rawKey: string): KeyName {\n  return keyNameMap[rawKey] || rawKey.toLowerCase()\n}\n\n// Export the key name mapping for use in UI components\nexport { keyNameMap }\n\n// Heartbeat utility functions\nfunction handleHeartbeat(_event: HeartbeatEvent) {\n  lastHeartbeatReceived = Date.now()\n}\n\nfunction startHeartbeatChecker() {\n  if (!heartbeatCheckTimer) {\n    heartbeatCheckTimer = setInterval(() => {\n      const timeSinceLastHeartbeat = Date.now() - lastHeartbeatReceived\n      if (timeSinceLastHeartbeat > HEARTBEAT_TIMEOUT_MS) {\n        console.error(\n          `[Key listener] No heartbeat received for ${timeSinceLastHeartbeat}ms, restarting key listener...`,\n        )\n        restartKeyListener()\n      }\n    }, HEARTBEAT_CHECK_INTERVAL_MS)\n  }\n}\n\nfunction stopHeartbeatChecker() {\n  if (heartbeatCheckTimer) {\n    clearInterval(heartbeatCheckTimer)\n    heartbeatCheckTimer = null\n  }\n}\n\nfunction restartKeyListener() {\n  console.warn('🔄 Restarting keyboard listener due to timeout...')\n  stopKeyListener()\n  // Wait a brief moment before restarting to ensure cleanup is complete\n  setTimeout(() => {\n    startKeyListener()\n  }, 1000)\n}\n\n// This set will track the state of all currently pressed keys.\nconst pressedKeys = new Set<string>()\n\n// Track when each key was first pressed to detect stuck keys\nconst keyPressTimestamps = new Map<KeyName, number>()\n\n// Timer for checking stuck keys\nlet stuckKeyCheckTimer: NodeJS.Timeout | null = null\n\n// Configuration for stuck key detection\nconst STUCK_KEY_TIMEOUT = 5000 // 5 seconds\nconst STUCK_KEY_CHECK_INTERVAL = 1000 // Check every 1 second\n\n// Function to check for and remove stuck keys\nfunction checkForStuckKeys() {\n  const currentTime = Date.now()\n  const stuckKeys: KeyName[] = []\n\n  for (const [key, pressTime] of keyPressTimestamps) {\n    if (currentTime - pressTime > STUCK_KEY_TIMEOUT) {\n      stuckKeys.push(key)\n    }\n  }\n\n  // Remove stuck keys, but be careful not to interfere with active shortcuts\n  for (const stuckKey of stuckKeys) {\n    // If there's an active shortcut, check if this stuck key is part of it\n    let shouldRemove = true\n\n    if (activeShortcutId !== null) {\n      const { keyboardShortcuts } = store.get(STORE_KEYS.SETTINGS)\n      const activeShortcut = keyboardShortcuts\n        .filter(ks => ks.keys.length > 0)\n        .find(shortcut => {\n          const normalizedShortcutKeys = shortcut.keys.map(normalizeLegacyKey)\n          const hasAllKeys = normalizedShortcutKeys.every(key =>\n            pressedKeys.has(key),\n          )\n          const exactMatch =\n            normalizedShortcutKeys.length === pressedKeys.size && hasAllKeys\n          return exactMatch\n        })\n\n      // Don't remove the stuck key if it's part of the currently active shortcut\n      if (\n        activeShortcut &&\n        activeShortcut.keys.map(normalizeLegacyKey).includes(stuckKey)\n      ) {\n        shouldRemove = false\n      }\n    }\n\n    if (shouldRemove) {\n      console.warn(\n        `Removing stuck key: ${stuckKey} (held for ${(currentTime - keyPressTimestamps.get(stuckKey)!) / 1000}s)`,\n      )\n      pressedKeys.delete(stuckKey)\n      keyPressTimestamps.delete(stuckKey)\n    }\n  }\n}\n\n// Start the stuck key checking timer\nfunction startStuckKeyChecker() {\n  if (!stuckKeyCheckTimer) {\n    stuckKeyCheckTimer = setInterval(\n      checkForStuckKeys,\n      STUCK_KEY_CHECK_INTERVAL,\n    )\n  }\n}\n\n// Stop the stuck key checking timer\nfunction stopStuckKeyChecker() {\n  if (stuckKeyCheckTimer) {\n    clearInterval(stuckKeyCheckTimer)\n    stuckKeyCheckTimer = null\n  }\n}\n\nasync function handleKeyEventInMain(event: KeyEvent) {\n  const { isShortcutGloballyEnabled, keyboardShortcuts } = store.get(\n    STORE_KEYS.SETTINGS,\n  )\n\n  if (!isShortcutGloballyEnabled) {\n    // check to see if we should stop an in-progress recording\n    if (activeShortcutId !== null) {\n      // Shortcut released\n      activeShortcutId = null\n      console.info('Shortcut DEACTIVATED, stopping recording...')\n      itoSessionManager.completeSession()\n    }\n    return\n  }\n\n  const normalizedKey = normalizeKey(event.key)\n\n  // Ignore the \"fast fn\" event which can be noisy.\n  if (normalizedKey === 'fn_fast') return\n\n  if (event.type === 'keydown') {\n    pressedKeys.add(normalizedKey)\n    // Track when this key was first pressed (only if not already tracked)\n    if (!keyPressTimestamps.has(normalizedKey)) {\n      keyPressTimestamps.set(normalizedKey, Date.now())\n    }\n  } else {\n    pressedKeys.delete(normalizedKey)\n    keyPressTimestamps.delete(normalizedKey)\n  }\n\n  // Check if any of the configured shortcuts are currently held\n  // Match shortcuts that have exactly the same keys as currently pressed\n  const currentlyHeldShortcut = keyboardShortcuts\n    .filter(ks => ks.keys.length > 0)\n    .find(shortcut => {\n      // Normalize legacy keys in stored shortcuts\n      const normalizedShortcutKeys = shortcut.keys.map(normalizeLegacyKey)\n\n      // Check if all shortcut keys are pressed (exact match only)\n      const hasAllKeys = normalizedShortcutKeys.every(shortcutKey =>\n        pressedKeys.has(shortcutKey),\n      )\n\n      const exactMatch =\n        normalizedShortcutKeys.length === pressedKeys.size && hasAllKeys\n\n      return exactMatch\n    })\n\n  // Handle shortcut activation and mode changes\n  if (currentlyHeldShortcut) {\n    if (activeShortcutId === null) {\n      // Starting a new session\n      activeShortcutId = currentlyHeldShortcut.id\n      console.info('lib Shortcut ACTIVATED, starting recording...')\n      await itoSessionManager.startSession(currentlyHeldShortcut.mode)\n    } else if (activeShortcutId !== currentlyHeldShortcut.id) {\n      // Different shortcut detected while already recording - change mode\n      activeShortcutId = currentlyHeldShortcut.id\n      console.info(\n        `lib Shortcut mode CHANGED to ${currentlyHeldShortcut.mode}, updating session...`,\n      )\n      itoSessionManager.setMode(currentlyHeldShortcut.mode)\n    }\n  } else if (!currentlyHeldShortcut) {\n    // No shortcut detected - cancel pending activation or deactivate active shortcut\n    if (activeShortcutId !== null) {\n      // Shortcut released - deactivate immediately (no debounce on release)\n      activeShortcutId = null\n      console.info('lib Shortcut DEACTIVATED, stopping recording...')\n      itoSessionManager.completeSession()\n    }\n  }\n}\n\n// Starts the key listener process\nexport const startKeyListener = () => {\n  if (KeyListenerProcess) {\n    console.warn('Key listener already running.')\n    return\n  }\n\n  const binaryPath = getNativeBinaryPath(nativeModuleName)\n  if (!binaryPath) {\n    console.error('Could not determine key listener binary path.')\n    return\n  }\n\n  console.log('--- Key Listener Initialization ---')\n  console.log(`Attempting to spawn key listener at: ${binaryPath}`)\n\n  try {\n    const env = {\n      ...process.env,\n      RUST_BACKTRACE: '1',\n      OBJC_DISABLE_INITIALIZE_FORK_SAFETY: 'YES',\n    }\n\n    KeyListenerProcess = spawn(binaryPath, [], {\n      stdio: ['pipe', 'pipe', 'pipe'],\n      env,\n      detached: true,\n    })\n\n    if (!KeyListenerProcess) {\n      throw new Error('Failed to spawn process')\n    }\n\n    KeyListenerProcess.unref()\n\n    let buffer = ''\n    KeyListenerProcess.stdout?.on('data', data => {\n      const chunk = data.toString()\n      buffer += chunk\n      const lines = buffer.split('\\n')\n      buffer = lines.pop() || ''\n      for (const line of lines) {\n        if (line.trim()) {\n          try {\n            const event: ProcessEvent = JSON.parse(line)\n\n            // Handle heartbeat and other system events\n            if (event.type === 'heartbeat_ping') {\n              handleHeartbeat(event)\n              continue\n            } else if (event.type === 'registered_hotkeys') {\n              // Log registered hotkeys for debugging\n              console.info('🔒 Registered hotkeys received:', event.hotkeys)\n              continue\n            }\n\n            // Handle regular key events\n            if (event.type === 'keydown' || event.type === 'keyup') {\n              // Process the event here in the main process for hotkey detection.\n              handleKeyEventInMain(event)\n\n              // Broadcast the raw event to all renderer windows for UI updates.\n              BrowserWindow.getAllWindows().forEach(window => {\n                if (!window.webContents.isDestroyed()) {\n                  window.webContents.send('key-event', event)\n                }\n              })\n            }\n          } catch (e) {\n            console.error('Failed to parse key process event:', line, e)\n          }\n        }\n      }\n    })\n\n    KeyListenerProcess.stderr?.on('data', data => {\n      console.error('[Key listener] stderr:', data.toString())\n    })\n\n    KeyListenerProcess.on('error', error => {\n      console.error('[Key listener] process spawn error:', error)\n      KeyListenerProcess = null\n    })\n\n    KeyListenerProcess.on('close', (code, signal) => {\n      console.warn(\n        `[Key listener] process closed with code: ${code}, signal: ${signal}`,\n      )\n      KeyListenerProcess = null\n    })\n\n    KeyListenerProcess.on('exit', (code, signal) => {\n      console.warn(\n        `[Key listener] process exited with code: ${code}, signal: ${signal}`,\n      )\n      KeyListenerProcess = null\n    })\n\n    console.log('[Key listener] started successfully.')\n\n    // Register all configured hotkeys with the listener\n    registerAllHotkeys()\n\n    // Start the stuck key checker\n    startStuckKeyChecker()\n\n    // Start heartbeat monitoring\n    lastHeartbeatReceived = Date.now()\n    startHeartbeatChecker()\n  } catch (error) {\n    console.error('Failed to start key listener:', error)\n    KeyListenerProcess = null\n  }\n}\n\n// Register all hotkeys from settings with the key listener\nexport const registerAllHotkeys = () => {\n  if (!KeyListenerProcess) {\n    console.warn('Key listener not running, cannot register hotkeys.')\n    return\n  }\n\n  const { keyboardShortcuts } = store.get(STORE_KEYS.SETTINGS)\n\n  // Convert shortcuts to hotkey format for the listener\n  const hotkeys = keyboardShortcuts\n    .filter(ks => ks.keys.length > 0)\n    .map(shortcut => ({\n      keys: getKeysToRegister(shortcut),\n    }))\n\n  console.info('Registering hotkeys with listener:', hotkeys)\n\n  KeyListenerProcess.stdin?.write(\n    JSON.stringify({ command: 'register_hotkeys', hotkeys }) + '\\n',\n  )\n}\n\n/**\n * A reverse mapping of normalized key names to their raw `rdev` counterparts.\n * This is a one-to-many relationship (e.g., 'command' maps to ['MetaLeft', 'MetaRight']).\n */\nconst reverseKeyNameMap: Record<string, string[]> = Object.entries(\n  keyNameMap,\n).reduce(\n  (acc, [rawKey, normalizedKey]) => {\n    if (!acc[normalizedKey]) {\n      acc[normalizedKey] = []\n    }\n    acc[normalizedKey].push(rawKey)\n    return acc\n  },\n  {} as Record<string, string[]>,\n)\n\nconst getKeysToRegister = (shortcut?: KeyboardShortcutConfig): string[] => {\n  if (!shortcut) {\n    return []\n  }\n\n  const keys: string[] = []\n\n  for (const key of shortcut.keys) {\n    // Normalize legacy keys (maps base modifiers to left variants)\n    const normalizedKey = normalizeLegacyKey(key)\n    const reverseMappedKeys = reverseKeyNameMap[normalizedKey]\n\n    if (reverseMappedKeys && reverseMappedKeys.length > 0) {\n      // Use the reverse mapping if available\n      keys.push(...reverseMappedKeys)\n    } else {\n      // Fallback: use the original key name as-is\n      // This works because the key names come from rdev originally\n      keys.push(key)\n    }\n  }\n\n  // Also block the special \"fast fn\" key if fn is part of the shortcut.\n  if (shortcut.keys.includes('fn')) {\n    keys.push('Unknown(179)')\n  }\n\n  // Return a unique set of keys.\n  return [...new Set(keys)]\n}\n\nexport const stopKeyListener = () => {\n  if (KeyListenerProcess) {\n    // Clear the set on stop to prevent stuck keys if the app restarts.\n    pressedKeys.clear()\n    keyPressTimestamps.clear()\n    stopStuckKeyChecker()\n\n    // Clean up heartbeat state\n    stopHeartbeatChecker()\n\n    KeyListenerProcess.kill('SIGTERM')\n    KeyListenerProcess = null\n  }\n}\n"
  },
  {
    "path": "lib/media/macOSAccessibilityContextProvider.ts",
    "content": "/**\n * macOS Accessibility Context Provider Implementation\n *\n * Uses a one-shot Swift binary that retrieves cursor context\n * using macOS NSAccessibility/AXUIElement APIs.\n */\n\nimport { execFile } from 'child_process'\nimport { platform, arch } from 'os'\nimport { getNativeBinaryPath } from './native-interface'\nimport log from 'electron-log'\nimport type { IAccessibilityContextProvider } from './IAccessibilityContextProvider'\nimport type {\n  CursorContextOptions,\n  CursorContextResult,\n} from '../types/cursorContext'\n\nconst NATIVE_MODULE_NAME = 'cursor-context'\nexport class MacOSAccessibilityContextProvider\n  implements IAccessibilityContextProvider\n{\n  #binaryPath: string | null = null\n\n  constructor() {}\n\n  public initialize(): void {\n    const binaryPath = getNativeBinaryPath(NATIVE_MODULE_NAME)\n    if (!binaryPath) {\n      const error = new Error(\n        `Cannot determine ${NATIVE_MODULE_NAME} binary path for platform ${platform()} and arch ${arch()}`,\n      )\n      log.error('[MacOSAccessibilityContextProvider]', error.message)\n      throw error\n    }\n\n    this.#binaryPath = binaryPath\n    console.log(\n      `[MacOSAccessibilityContextProvider] Initialized with binary path: ${binaryPath}`,\n    )\n  }\n\n  public shutdown(): void {\n    // No-op for one-shot process\n  }\n\n  public isRunning(): boolean {\n    return this.#binaryPath !== null\n  }\n\n  public async getCursorContext(\n    options: CursorContextOptions,\n  ): Promise<CursorContextResult> {\n    if (!this.#binaryPath) {\n      throw new Error('Provider not initialized. Call initialize() first.')\n    }\n\n    return new Promise((resolve, reject) => {\n      const args = [\n        '--before',\n        String(options.maxCharsBefore),\n        '--after',\n        String(options.maxCharsAfter),\n      ]\n\n      // Enable debug logging if requested\n      if (options.debug) {\n        args.push('--debug')\n      }\n\n      execFile(\n        this.#binaryPath!,\n        args,\n        { timeout: options.timeout },\n        (error, stdout, stderr) => {\n          if (error) {\n            log.error(\n              '[MacOSAccessibilityContextProvider] execFile error:',\n              error,\n            )\n            reject(error)\n            return\n          }\n\n          if (stderr) {\n            console.log(\n              '[MacOSAccessibilityContextProvider] stderr:',\n              stderr.trim(),\n            )\n          }\n\n          try {\n            const result: CursorContextResult = JSON.parse(stdout.trim())\n            console.log(\n              '[MacOSAccessibilityContextProvider] Retrieved cursor context:',\n              result,\n            )\n            resolve(result)\n          } catch (parseError) {\n            log.error(\n              '[MacOSAccessibilityContextProvider] Failed to parse JSON:',\n              parseError,\n            )\n            reject(new Error('Failed to parse response from native binary'))\n          }\n        },\n      )\n    })\n  }\n}\n\n// Export singleton instance\nexport const macOSAccessibilityContextProvider =\n  new MacOSAccessibilityContextProvider()\n"
  },
  {
    "path": "lib/media/microphoneSetUp.ts",
    "content": "import mainStore from '../main/store'\nimport { STORE_KEYS } from '../constants/store-keys'\nimport { audioRecorderService } from './audio'\n\n/**\n * Initializes the microphone selection to prefer built-in microphone over system default\n */\nexport const initializeMicrophoneSelection = async () => {\n  try {\n    console.log(\n      '[initializeMicrophoneSelection] Initializing microphone selection...',\n    )\n\n    // Get current settings from store\n    const currentSettings = mainStore.get(STORE_KEYS.SETTINGS)\n\n    // If user has already selected a specific microphone (not default), keep their choice\n    if (\n      currentSettings?.microphoneDeviceId &&\n      currentSettings.microphoneDeviceId !== 'default'\n    ) {\n      console.log(\n        `[initializeMicrophoneSelection] User has selected \"${currentSettings.microphoneDeviceId}\". Keeping their choice.`,\n      )\n      return\n    }\n\n    const availableDevices = await audioRecorderService.getDeviceList()\n\n    // Look for built-in microphone\n    const builtInDevice = availableDevices.find(device => {\n      const deviceLower = device.toLowerCase()\n      return (\n        deviceLower.includes('built-in') ||\n        deviceLower.includes('internal') ||\n        deviceLower.includes('macbook') ||\n        deviceLower.includes('system')\n      )\n    })\n\n    if (builtInDevice) {\n      console.log(\n        `[initializeMicrophoneSelection] Setting built-in microphone as default: \"${builtInDevice}\"`,\n      )\n\n      // Update the settings in the store\n      const updatedSettings = {\n        ...currentSettings,\n        microphoneDeviceId: builtInDevice,\n        microphoneName: 'Built-in mic (recommended)',\n      }\n      mainStore.set(STORE_KEYS.SETTINGS, updatedSettings)\n    } else {\n      console.log(\n        '[initializeMicrophoneSelection] No built-in microphone found. Keeping \"Auto-detect\".',\n      )\n    }\n  } catch (error) {\n    console.error(\n      '[initializeMicrophoneSelection] Failed to initialize microphone selection:',\n      error,\n    )\n  }\n}\n"
  },
  {
    "path": "lib/media/native-interface.test.ts",
    "content": "import { describe, test, expect, beforeEach, mock } from 'bun:test'\n\n// Mock all external dependencies\nconst mockJoin = mock((...paths: string[]) => paths.join('/'))\nmock.module('path', () => ({\n  join: mockJoin,\n}))\n// Some environments resolve to node:path; mock that as well\nmock.module('node:path', () => ({\n  join: mockJoin,\n}))\n\nconst mockApp = {\n  isPackaged: false,\n}\nmock.module('electron', () => ({\n  app: mockApp,\n}))\n\nconst mockOs = {\n  platform: mock(() => 'darwin'),\n  arch: mock(() => 'arm64'),\n}\nmock.module('os', () => ({\n  default: mockOs,\n}))\n\n// Mock console to avoid spam\nbeforeEach(() => {\n  console.error = mock()\n})\n\ndescribe('Native Interface Module', () => {\n  // NOTE: Dynamic imports (await import('./native-interface')) are required because\n  // the module uses top-level constants that are evaluated at import time:\n  // - const platform = os.platform()\n  // - const isDev = !app.isPackaged\n  // These need to be mocked before the module is imported to ensure tests use\n  // the correct mocked values instead of real platform detection.\n  beforeEach(() => {\n    // Reset all mocks\n    mockJoin.mockClear()\n    mockOs.platform.mockClear()\n    mockOs.arch.mockClear()\n\n    // Reset module state\n    delete require.cache[require.resolve('./native-interface')]\n\n    // Set default platform and arch\n    mockOs.platform.mockReturnValue('darwin')\n    mockOs.arch.mockReturnValue('arm64')\n    mockApp.isPackaged = false\n  })\n\n  describe('Platform-Specific Path Resolution Business Logic', () => {\n    test('should resolve Darwin development binary path correctly', async () => {\n      mockOs.platform.mockReturnValue('darwin')\n      mockOs.arch.mockReturnValue('arm64')\n      // ensure dev mode\n      mockApp.isPackaged = false\n      const { getNativeBinaryPath } = await import('./native-interface')\n\n      const result = getNativeBinaryPath('global-key-listener')\n\n      expect(mockJoin).toHaveBeenLastCalledWith(\n        expect.stringContaining('native/target/aarch64-apple-darwin/release'),\n        'global-key-listener',\n      )\n      expect(result).toContain(\n        'aarch64-apple-darwin/release/global-key-listener',\n      )\n    })\n\n    test('should resolve Windows development binary path correctly', async () => {\n      mockOs.platform.mockReturnValue('win32')\n      mockApp.isPackaged = false\n      const { getNativeBinaryPath } = await import('./native-interface')\n\n      const result = getNativeBinaryPath('audio-recorder')\n\n      expect(mockJoin).toHaveBeenLastCalledWith(\n        expect.stringContaining('native/target/x86_64-pc-windows-msvc/release'),\n        'audio-recorder.exe',\n      )\n      expect(result).toContain(\n        'x86_64-pc-windows-msvc/release/audio-recorder.exe',\n      )\n    })\n\n    test('should handle unsupported development platforms gracefully', async () => {\n      mockOs.platform.mockReturnValue('linux')\n      mockApp.isPackaged = false\n      const { getNativeBinaryPath } = await import('./native-interface')\n\n      const result = getNativeBinaryPath('test-module')\n\n      expect(console.error).toHaveBeenCalledWith(\n        'Cannot determine test-module binary path for platform linux',\n      )\n      expect(result).toBeNull()\n    })\n\n    test('should handle FreeBSD development platform gracefully', async () => {\n      mockOs.platform.mockReturnValue('freebsd')\n      mockApp.isPackaged = false\n      const { getNativeBinaryPath } = await import('./native-interface')\n\n      const result = getNativeBinaryPath('test-module')\n\n      expect(console.error).toHaveBeenCalledWith(\n        'Cannot determine test-module binary path for platform freebsd',\n      )\n      expect(result).toBeNull()\n    })\n  })\n\n  describe('Production Mode Business Logic', () => {\n    test('should resolve production binary path for any platform', async () => {\n      mockApp.isPackaged = true\n      mockOs.platform.mockReturnValue('darwin')\n\n      const originalResourcesPath = process.resourcesPath\n      Object.defineProperty(process, 'resourcesPath', {\n        value: '/app/resources',\n        configurable: true,\n      })\n\n      try {\n        const { getNativeBinaryPath } = await import('./native-interface')\n\n        const result = getNativeBinaryPath('global-key-listener')\n\n        expect(mockJoin).toHaveBeenLastCalledWith(\n          '/app/resources/binaries',\n          'global-key-listener',\n        )\n        expect(result).toBe('/app/resources/binaries/global-key-listener')\n      } finally {\n        Object.defineProperty(process, 'resourcesPath', {\n          value: originalResourcesPath,\n          configurable: true,\n        })\n      }\n    })\n\n    test('should add .exe extension for Windows in production', async () => {\n      mockApp.isPackaged = true\n      mockOs.platform.mockReturnValue('win32')\n\n      const originalResourcesPath = process.resourcesPath\n      Object.defineProperty(process, 'resourcesPath', {\n        value: 'C:\\\\app\\\\resources',\n        configurable: true,\n      })\n\n      try {\n        const { getNativeBinaryPath } = await import('./native-interface')\n\n        const result = getNativeBinaryPath('audio-recorder')\n\n        expect(mockJoin).toHaveBeenLastCalledWith(\n          'C:\\\\app\\\\resources/binaries',\n          'audio-recorder.exe',\n        )\n        expect(result).toBe('C:\\\\app\\\\resources/binaries/audio-recorder.exe')\n      } finally {\n        Object.defineProperty(process, 'resourcesPath', {\n          value: originalResourcesPath,\n          configurable: true,\n        })\n      }\n    })\n\n    test('should handle missing resourcesPath in production gracefully', async () => {\n      mockApp.isPackaged = true\n\n      const originalResourcesPath = process.resourcesPath\n      delete (process as any).resourcesPath\n\n      try {\n        const { getNativeBinaryPath } = await import('./native-interface')\n\n        const result = getNativeBinaryPath('test-module')\n\n        // Should still work but may use undefined path\n        expect(mockJoin).toHaveBeenCalled()\n        expect(result).toBeTypeOf('string')\n      } finally {\n        if (originalResourcesPath) {\n          Object.defineProperty(process, 'resourcesPath', {\n            value: originalResourcesPath,\n            configurable: true,\n          })\n        }\n      }\n    })\n  })\n\n  describe('Development vs Production Mode Business Logic', () => {\n    test('should use different path strategies for development vs production', async () => {\n      mockOs.platform.mockReturnValue('darwin')\n\n      // Test development mode\n      mockApp.isPackaged = false\n      delete require.cache[require.resolve('./native-interface')]\n      const devImport = await import('./native-interface')\n      const devResult = devImport.getNativeBinaryPath('test-module')\n\n      // Test production mode\n      mockApp.isPackaged = true\n      const originalResourcesPath = process.resourcesPath\n      Object.defineProperty(process, 'resourcesPath', {\n        value: '/app/resources',\n        configurable: true,\n      })\n\n      try {\n        delete require.cache[require.resolve('./native-interface')]\n        const prodImport = await import('./native-interface')\n        const prodResult = prodImport.getNativeBinaryPath('test-module')\n\n        expect(devResult).toContain('target/aarch64-apple-darwin/release')\n        expect(prodResult).toContain('resources/binaries')\n        expect(devResult).not.toBe(prodResult)\n      } finally {\n        Object.defineProperty(process, 'resourcesPath', {\n          value: originalResourcesPath,\n          configurable: true,\n        })\n      }\n    })\n  })\n\n  describe('Error Handling Business Logic', () => {\n    test('should return null and log error for unsupported platforms', async () => {\n      mockOs.platform.mockReturnValue('unsupported-platform')\n      const { getNativeBinaryPath } = await import('./native-interface')\n\n      const result = getNativeBinaryPath('test-module')\n\n      expect(console.error).toHaveBeenCalledWith(\n        'Cannot determine test-module binary path for platform unsupported-platform',\n      )\n      expect(result).toBeNull()\n    })\n\n    test('should handle different module names correctly', async () => {\n      mockOs.platform.mockReturnValue('darwin')\n      const { getNativeBinaryPath } = await import('./native-interface')\n\n      const result1 = getNativeBinaryPath('audio-recorder')\n      const result2 = getNativeBinaryPath('global-key-listener')\n\n      expect(result1).toContain('audio-recorder')\n      expect(result2).toContain('global-key-listener')\n      expect(result1).not.toBe(result2)\n    })\n  })\n})\n"
  },
  {
    "path": "lib/media/native-interface.ts",
    "content": "import { app } from 'electron'\nimport os from 'os'\nimport { join } from 'path'\n\nconst platform = os.platform()\nconst isDev = !app.isPackaged\n\nexport const getNativeBinaryPath = (\n  nativeModuleName: string,\n): string | null => {\n  const targetDir = getTargetDir()\n  const binaryName =\n    platform === 'win32' ? `${nativeModuleName}.exe` : `${nativeModuleName}`\n\n  if (!targetDir) {\n    console.error(\n      `Cannot determine ${nativeModuleName} binary path for platform ${platform}`,\n    )\n    return null\n  }\n  return join(targetDir, binaryName)\n}\n\nconst getTargetDir = (): string | null => {\n  if (isDev) {\n    const targetBase = join(__dirname, '../../native/target')\n\n    if (platform === 'darwin') {\n      // Detect current architecture\n      const arch = os.arch() // 'arm64' or 'x64'\n      const cargoArch = arch === 'arm64' ? 'aarch64' : 'x86_64'\n      const targetDir = join(targetBase, `${cargoArch}-apple-darwin/release`)\n      return targetDir\n    } else if (platform === 'win32') {\n      return join(targetBase, 'x86_64-pc-windows-msvc/release')\n    }\n    // Fallback for unsupported dev platforms\n    return null\n  }\n  return join(process.resourcesPath, 'binaries')\n}\n"
  },
  {
    "path": "lib/media/selected-text-reader.test.ts",
    "content": "import { describe, test, expect, beforeEach, mock, afterEach } from 'bun:test'\nimport { EventEmitter } from 'events'\n\n// Mock all external dependencies\nconst mockStdout = new EventEmitter()\nconst mockStderr = new EventEmitter()\nmockStdout.on = mock(mockStdout.on.bind(mockStdout))\nmockStderr.on = mock(mockStderr.on.bind(mockStderr))\n\nconst mockChildProcess = {\n  stdin: {\n    write: mock(),\n  },\n  stdout: mockStdout,\n  stderr: mockStderr,\n  on: mock((event: string, handler: any) => {\n    // Store handlers so tests can verify they were registered\n    if (event === 'close') {\n      mockChildProcess._closeHandler = handler as (\n        code: number,\n        signal: string,\n      ) => void\n    } else if (event === 'error') {\n      mockChildProcess._errorHandler = handler as (err: Error) => void\n    }\n  }),\n  kill: mock(),\n  pid: 12345,\n  _closeHandler: null as ((code: number, signal: string) => void) | null,\n  _errorHandler: null as ((err: Error) => void) | null,\n}\n\nconst mockSpawn = mock(() => mockChildProcess)\n\nmock.module('child_process', () => ({\n  spawn: mockSpawn,\n}))\n\nconst mockLog = {\n  info: mock(),\n  warn: mock(),\n  error: mock(),\n  debug: mock(),\n}\n\nmock.module('electron-log', () => ({\n  default: mockLog,\n}))\n\nconst selectedTextReaderPath = '/mock/path/to/selected-text-reader'\n\nconst mockGetNativeBinaryPath = mock(\n  (): string | null => selectedTextReaderPath,\n)\n\nmock.module('./native-interface', () => ({\n  getNativeBinaryPath: mockGetNativeBinaryPath,\n}))\n\n// Import after mocking\nimport {\n  selectedTextReaderService,\n  getSelectedText,\n  getSelectedTextString,\n  hasSelectedText,\n} from './selected-text-reader'\n\ndescribe('SelectedTextReaderService', () => {\n  beforeEach(() => {\n    // Reset all mocks\n    mockSpawn.mockClear()\n    mockChildProcess.stdin.write.mockClear()\n    mockChildProcess.on.mockClear()\n    mockChildProcess.kill.mockClear()\n    mockGetNativeBinaryPath.mockClear()\n    mockLog.info.mockClear()\n    mockLog.warn.mockClear()\n    mockLog.error.mockClear()\n    mockLog.debug.mockClear()\n\n    // Reset mock return value to the expected path\n    mockGetNativeBinaryPath.mockReturnValue(selectedTextReaderPath)\n\n    // Clear event emitter listeners to prevent memory leaks\n    mockStdout.removeAllListeners()\n    mockStderr.removeAllListeners()\n\n    // Reset stdin.write mock to default\n    mockChildProcess.stdin.write.mockReset()\n  })\n\n  afterEach(() => {\n    // Clean up service state\n    selectedTextReaderService.terminate()\n  })\n\n  test('should initialize service and spawn process', () => {\n    selectedTextReaderService.initialize()\n\n    expect(mockSpawn).toHaveBeenCalledWith(selectedTextReaderPath, [], {\n      stdio: ['pipe', 'pipe', 'pipe'],\n    })\n    expect(mockChildProcess.stdout.on).toHaveBeenCalledWith(\n      'data',\n      expect.any(Function),\n    )\n    expect(mockChildProcess.on).toHaveBeenCalledWith(\n      'close',\n      expect.any(Function),\n    )\n    expect(mockChildProcess.on).toHaveBeenCalledWith(\n      'error',\n      expect.any(Function),\n    )\n  })\n\n  test('should not initialize if already running', () => {\n    selectedTextReaderService.initialize()\n    mockSpawn.mockClear()\n\n    selectedTextReaderService.initialize()\n\n    expect(mockSpawn).not.toHaveBeenCalled()\n    expect(mockLog.warn).toHaveBeenCalledWith(\n      '[SelectedTextService] Selected text reader already running.',\n    )\n  })\n\n  test('should handle missing binary path', () => {\n    mockGetNativeBinaryPath.mockReturnValue(null)\n\n    // Add error event listener to handle the emitted error\n    selectedTextReaderService.on('error', () => {\n      // Expected error, do nothing\n    })\n\n    selectedTextReaderService.initialize()\n\n    expect(mockSpawn).not.toHaveBeenCalled()\n    expect(mockLog.error).toHaveBeenCalledWith(\n      expect.stringContaining(\n        'Cannot determine selected-text-reader binary path',\n      ),\n    )\n  })\n\n  test('should terminate process', () => {\n    selectedTextReaderService.initialize()\n    selectedTextReaderService.terminate()\n\n    expect(mockChildProcess.kill).toHaveBeenCalled()\n  })\n\n  test('should send command to get selected text', async () => {\n    selectedTextReaderService.initialize()\n\n    let capturedRequestId: string | null = null\n\n    // Capture the request ID from stdin write\n    mockChildProcess.stdin.write.mockImplementation((data: string) => {\n      try {\n        const command = JSON.parse(data.trim())\n        capturedRequestId = command.requestId\n\n        // Simulate response with the captured request ID\n        setTimeout(() => {\n          const mockResponse = {\n            requestId: capturedRequestId,\n            success: true,\n            text: 'selected text',\n            error: null,\n            length: 13,\n          }\n          mockChildProcess.stdout.emit(\n            'data',\n            Buffer.from(JSON.stringify(mockResponse) + '\\n'),\n          )\n        }, 5)\n      } catch {\n        // Ignore parsing errors for this test\n      }\n      return true\n    })\n\n    const promise = selectedTextReaderService.getSelectedText({\n      format: 'json',\n      maxLength: 1000,\n    })\n\n    const result = await promise\n    expect(result.success).toBe(true)\n    expect(result.text).toBe('selected text')\n    expect(capturedRequestId).toBeTruthy()\n  })\n\n  test('should handle process not running', async () => {\n    const promise = selectedTextReaderService.getSelectedText({\n      format: 'json',\n      maxLength: 1000,\n    })\n\n    expect(promise).rejects.toThrow('Selected text reader process not running')\n  })\n\n  test('should handle process close with pending requests', () => {\n    selectedTextReaderService.initialize()\n\n    const promise = selectedTextReaderService.getSelectedText({\n      format: 'json',\n      maxLength: 1000,\n    })\n\n    // Simulate process close\n    if (mockChildProcess._closeHandler) {\n      mockChildProcess._closeHandler(1, 'SIGTERM')\n    }\n\n    expect(promise).rejects.toThrow('Process exited with code 1')\n  })\n})\n\ndescribe('Selected Text Reader Functions', () => {\n  beforeEach(() => {\n    mockSpawn.mockClear()\n    mockChildProcess.stdin.write.mockClear()\n    mockLog.debug.mockClear()\n    mockLog.error.mockClear()\n\n    // Reset mock return value to the expected path\n    mockGetNativeBinaryPath.mockReturnValue(selectedTextReaderPath)\n\n    // Clear event emitter listeners\n    mockStdout.removeAllListeners()\n    mockStderr.removeAllListeners()\n\n    // Reset stdin.write mock\n    mockChildProcess.stdin.write.mockReset()\n  })\n\n  afterEach(() => {\n    selectedTextReaderService.terminate()\n  })\n\n  test('getSelectedText should use service', async () => {\n    selectedTextReaderService.initialize()\n\n    // Mock stdin write to respond with captured request ID\n    mockChildProcess.stdin.write.mockImplementation((data: string) => {\n      try {\n        const command = JSON.parse(data.trim())\n        setTimeout(() => {\n          const mockResponse = {\n            requestId: command.requestId,\n            success: true,\n            text: 'test text',\n            error: null,\n            length: 9,\n          }\n          mockChildProcess.stdout.emit(\n            'data',\n            Buffer.from(JSON.stringify(mockResponse) + '\\n'),\n          )\n        }, 5)\n      } catch {\n        // Ignore parsing errors\n      }\n      return true\n    })\n\n    const result = await getSelectedText({ format: 'json', maxLength: 1000 })\n\n    expect(result.success).toBe(true)\n    expect(result.text).toBe('test text')\n  })\n\n  test('getSelectedTextString should return text or null', async () => {\n    selectedTextReaderService.initialize()\n\n    mockChildProcess.stdin.write.mockImplementation((data: string) => {\n      try {\n        const command = JSON.parse(data.trim())\n        setTimeout(() => {\n          const mockResponse = {\n            requestId: command.requestId,\n            success: true,\n            text: 'hello world',\n            error: null,\n            length: 11,\n          }\n          mockChildProcess.stdout.emit(\n            'data',\n            Buffer.from(JSON.stringify(mockResponse) + '\\n'),\n          )\n        }, 5)\n      } catch {\n        // Ignore parsing errors\n      }\n      return true\n    })\n\n    const result = await getSelectedTextString(1000)\n\n    expect(result).toBe('hello world')\n  })\n\n  test('getSelectedTextString should return null on failure', async () => {\n    selectedTextReaderService.initialize()\n\n    mockChildProcess.stdin.write.mockImplementation((data: string) => {\n      try {\n        const command = JSON.parse(data.trim())\n        setTimeout(() => {\n          const mockResponse = {\n            requestId: command.requestId,\n            success: false,\n            text: null,\n            error: 'No text selected',\n            length: 0,\n          }\n          mockChildProcess.stdout.emit(\n            'data',\n            Buffer.from(JSON.stringify(mockResponse) + '\\n'),\n          )\n        }, 5)\n      } catch {\n        // Ignore parsing errors\n      }\n      return true\n    })\n\n    const result = await getSelectedTextString(1000)\n\n    expect(result).toBeNull()\n  })\n\n  test('hasSelectedText should return boolean', async () => {\n    selectedTextReaderService.initialize()\n\n    mockChildProcess.stdin.write.mockImplementation((data: string) => {\n      try {\n        const command = JSON.parse(data.trim())\n        setTimeout(() => {\n          const mockResponse = {\n            requestId: command.requestId,\n            success: true,\n            text: 'x',\n            error: null,\n            length: 1,\n          }\n          mockChildProcess.stdout.emit(\n            'data',\n            Buffer.from(JSON.stringify(mockResponse) + '\\n'),\n          )\n        }, 5)\n      } catch {\n        // Ignore parsing errors\n      }\n      return true\n    })\n\n    const result = await hasSelectedText()\n\n    expect(typeof result).toBe('boolean')\n    expect(result).toBe(true)\n  })\n\n  test('hasSelectedText should return false when no text', async () => {\n    selectedTextReaderService.initialize()\n\n    mockChildProcess.stdin.write.mockImplementation((data: string) => {\n      try {\n        const command = JSON.parse(data.trim())\n        setTimeout(() => {\n          const mockResponse = {\n            requestId: command.requestId,\n            success: true,\n            text: null,\n            error: null,\n            length: 0,\n          }\n          mockChildProcess.stdout.emit(\n            'data',\n            Buffer.from(JSON.stringify(mockResponse) + '\\n'),\n          )\n        }, 5)\n      } catch {\n        // Ignore parsing errors\n      }\n      return true\n    })\n\n    const result = await hasSelectedText()\n\n    expect(result).toBe(false)\n  })\n\n  test('functions should handle service errors gracefully', async () => {\n    // Don't initialize service to test error handling\n\n    const textResult = await getSelectedTextString(1000)\n    expect(textResult).toBeNull()\n    expect(mockLog.error).toHaveBeenCalledWith(\n      'Error getting selected text:',\n      expect.any(Error),\n    )\n\n    const hasResult = await hasSelectedText()\n    expect(hasResult).toBe(false)\n  })\n})\n"
  },
  {
    "path": "lib/media/selected-text-reader.ts",
    "content": "import { spawn } from 'child_process'\nimport { platform, arch } from 'os'\nimport { getNativeBinaryPath } from './native-interface'\nimport log from 'electron-log'\nimport { EventEmitter } from 'events'\ninterface SelectedTextOptions {\n  format?: 'json' | 'text' // Output format\n  maxLength?: number // Maximum length of text to return\n}\n\ninterface SelectedTextResult {\n  success: boolean\n  text: string | null\n  error: string | null\n  length: number\n}\n\ninterface SelectedTextCommand {\n  command: 'get-text'\n  format?: 'json' | 'text'\n  maxLength?: number\n  requestId: string\n}\n\nexport interface CursorContextResult {\n  success: boolean\n  contextText: string | null\n  error: string | null\n  length: number\n}\n\ninterface CursorContextCommand {\n  command: 'get-cursor-context'\n  cutCurrentSelection?: boolean // Whether to cut current selection to position cursor correctly\n  contextLength?: number\n  requestId: string\n}\n\nconst nativeModuleName = 'selected-text-reader'\nconst MAXIUMUM_TEXT_LENGTH_DEFAULT = 10000 // Maximum length of text to return\n\ntype PendingRequest = {\n  resolve: (value: any) => void\n  reject: (reason?: any) => void\n}\n\nclass SelectedTextReaderService extends EventEmitter {\n  #selectedTextProcess: ReturnType<typeof spawn> | null = null\n  #pendingRequests = new Map<string, PendingRequest>()\n  #requestIdCounter = 0\n\n  constructor() {\n    super()\n  }\n\n  /**\n   * Spawns and initializes the native selected-text-reader process.\n   */\n  public initialize(): void {\n    if (this.#selectedTextProcess) {\n      log.warn('[SelectedTextService] Selected text reader already running.')\n      return\n    }\n\n    const binaryPath = getNativeBinaryPath(nativeModuleName)\n    if (!binaryPath) {\n      log.error(\n        `[SelectedTextService] Cannot determine ${nativeModuleName} binary path for platform ${platform()} and arch ${arch()}`,\n      )\n      this.emit('error', new Error('Selected text reader binary not found.'))\n      return\n    }\n\n    console.log(\n      `[SelectedTextService] Spawning selected text reader at: ${binaryPath}`,\n    )\n    try {\n      this.#selectedTextProcess = spawn(binaryPath, [], {\n        stdio: ['pipe', 'pipe', 'pipe'],\n      })\n\n      if (!this.#selectedTextProcess) {\n        throw new Error('Failed to spawn process')\n      }\n\n      this.#selectedTextProcess.stdout?.on('data', this.#onData.bind(this))\n      this.#selectedTextProcess.stderr?.on('data', this.#onStdErr.bind(this))\n      this.#selectedTextProcess.on('close', this.#onClose.bind(this))\n      this.#selectedTextProcess.on('error', this.#onError.bind(this))\n\n      console.log('[SelectedTextService] Selected text reader process started.')\n    } catch (err) {\n      log.error(\n        '[SelectedTextService] Caught an error while spawning selected text reader:',\n        err,\n      )\n      this.#selectedTextProcess = null\n      this.emit('error', err)\n    }\n  }\n\n  /**\n   * Stops the native selected-text-reader process.\n   */\n  public terminate(): void {\n    if (this.#selectedTextProcess) {\n      console.log(\n        '[SelectedTextService] Stopping selected text reader process.',\n      )\n      this.#selectedTextProcess.kill()\n      this.#selectedTextProcess = null\n      this.emit('stopped')\n\n      // Reject all pending requests\n      this.#pendingRequests.forEach(({ reject }) => {\n        reject(new Error('Service terminated'))\n      })\n      this.#pendingRequests.clear()\n    }\n  }\n\n  /**\n   * Sends a command to get selected text.\n   */\n  public async getSelectedText(\n    options: SelectedTextOptions = {\n      format: 'json',\n      maxLength: MAXIUMUM_TEXT_LENGTH_DEFAULT,\n    },\n  ): Promise<SelectedTextResult> {\n    if (!this.#selectedTextProcess) {\n      throw new Error('Selected text reader process not running')\n    }\n\n    return new Promise((resolve, reject) => {\n      const requestId = `req_${++this.#requestIdCounter}_${Date.now()}`\n      this.#pendingRequests.set(requestId, { resolve, reject })\n\n      const command: SelectedTextCommand = {\n        command: 'get-text',\n        format: options.format || 'json',\n        maxLength: options.maxLength || MAXIUMUM_TEXT_LENGTH_DEFAULT,\n        requestId,\n      }\n\n      this.#sendCommand(command)\n\n      // Set timeout to avoid hanging requests\n      setTimeout(() => {\n        if (this.#pendingRequests.has(requestId)) {\n          this.#pendingRequests.delete(requestId)\n          reject(new Error('Selected text request timed out'))\n        }\n      }, 5000) // 5 second timeout\n    })\n  }\n\n  /**\n   * Sends a command to get cursor context.\n   */\n  public async getCursorContext(\n    contextLength: number,\n    cutCurrentSelection: boolean = false,\n  ): Promise<CursorContextResult> {\n    if (!this.#selectedTextProcess) {\n      throw new Error('Selected text reader process not running')\n    }\n\n    return new Promise((resolve, reject) => {\n      const requestId = `ctx_${++this.#requestIdCounter}_${Date.now()}`\n      this.#pendingRequests.set(requestId, { resolve, reject })\n\n      const command: CursorContextCommand = {\n        command: 'get-cursor-context',\n        contextLength: contextLength,\n        cutCurrentSelection,\n        requestId,\n      }\n\n      this.#sendCommand(command)\n\n      // Set timeout to avoid hanging requests\n      setTimeout(() => {\n        if (this.#pendingRequests.has(requestId)) {\n          this.#pendingRequests.delete(requestId)\n          reject(new Error('Cursor context request timed out'))\n        }\n      }, 5000) // 5 second timeout\n    })\n  }\n\n  #sendCommand(command: SelectedTextCommand | CursorContextCommand): void {\n    if (!this.#selectedTextProcess) {\n      log.error(\n        '[SelectedTextService] Cannot send command, process not running',\n      )\n      return\n    }\n\n    try {\n      const commandStr = JSON.stringify(command) + '\\n'\n      this.#selectedTextProcess.stdin?.write(commandStr)\n    } catch (error) {\n      log.error('[SelectedTextService] Error sending command:', error)\n    }\n  }\n\n  #onData(data: Buffer): void {\n    const lines = data.toString().trim().split('\\n')\n\n    for (const line of lines) {\n      if (!line.trim()) continue\n\n      try {\n        const response = JSON.parse(line)\n\n        if (\n          response.requestId &&\n          this.#pendingRequests.has(response.requestId)\n        ) {\n          const { resolve } = this.#pendingRequests.get(response.requestId)!\n          this.#pendingRequests.delete(response.requestId)\n          resolve(response)\n        } else {\n          log.warn(\n            '[SelectedTextService] Received response for unknown request:',\n            response.requestId,\n          )\n        }\n      } catch (error) {\n        log.error(\n          '[SelectedTextService] Error parsing response:',\n          error,\n          'Raw data:',\n          line,\n        )\n      }\n    }\n  }\n\n  #onStdErr(data: Buffer): void {\n    log.error('[SelectedTextService] stderr:', data.toString())\n  }\n\n  #onClose(code: number, signal: string): void {\n    log.warn(\n      `[SelectedTextService] Process exited with code: ${code}, signal: ${signal}`,\n    )\n    this.#selectedTextProcess = null\n\n    // Reject all pending requests\n    this.#pendingRequests.forEach(({ reject }) => {\n      reject(new Error(`Process exited with code ${code}`))\n    })\n    this.#pendingRequests.clear()\n\n    this.emit('closed', { code, signal })\n  }\n\n  #onError(error: Error): void {\n    log.error('[SelectedTextService] Process error:', error)\n    this.emit('error', error)\n  }\n\n  public isRunning(): boolean {\n    return this.#selectedTextProcess !== null\n  }\n}\n\n// Export singleton instance\nexport const selectedTextReaderService = new SelectedTextReaderService()\n\nexport function getSelectedText(\n  options: SelectedTextOptions = {\n    format: 'json',\n    maxLength: MAXIUMUM_TEXT_LENGTH_DEFAULT,\n  },\n): Promise<SelectedTextResult> {\n  return selectedTextReaderService.getSelectedText(options)\n}\n\n/**\n * Get selected text as plain string (convenience method)\n */\nexport async function getSelectedTextString(\n  maxLength: number = MAXIUMUM_TEXT_LENGTH_DEFAULT,\n): Promise<string | null> {\n  try {\n    const result = await selectedTextReaderService.getSelectedText({\n      format: 'json',\n      maxLength,\n    })\n    return result.success ? result.text : null\n  } catch (error) {\n    log.error('Error getting selected text:', error)\n    return null\n  }\n}\n\n/**\n * Check if there is any selected text available\n */\nexport async function hasSelectedText(): Promise<boolean> {\n  try {\n    const result = await selectedTextReaderService.getSelectedText({\n      format: 'json',\n      maxLength: 1,\n    })\n    return result.success && result.length > 0\n  } catch (error) {\n    log.error('Error checking for selected text:', error)\n    return false\n  }\n}\n\n/**\n * Get cursor context using the new getCursorContext functionality\n */\nexport async function getCursorContext(contextLength: number): Promise<string> {\n  const cursorContextResult = await selectedTextReaderService.getCursorContext(\n    contextLength,\n    false,\n  )\n  const preCursorText = cursorContextResult.contextText || ''\n\n  return preCursorText\n}\n"
  },
  {
    "path": "lib/media/systemAudio.ts",
    "content": "import { execSync } from 'child_process'\nimport log from 'electron-log'\nimport os from 'os'\n\nlet previousVolume: number | null = null\n\n/**\n * Gets the current system volume (0-100)\n */\nexport function getSystemVolume(): number | null {\n  if (os.platform() !== 'darwin') {\n    log.warn('System audio control is only supported on macOS')\n    return null\n  }\n\n  try {\n    const result = execSync(\n      'osascript -e \"get volume settings\" | grep -o \"output volume:[0-9]*\" | grep -o \"[0-9]*\"',\n      { encoding: 'utf8' },\n    )\n    return parseInt(result.trim(), 10)\n  } catch (error) {\n    log.error('Failed to get system volume:', error)\n    return null\n  }\n}\n\n/**\n * Sets the system volume (0-100)\n */\nexport function setSystemVolume(volume: number): boolean {\n  if (os.platform() !== 'darwin') {\n    log.warn('System audio control is only supported on macOS')\n    return false\n  }\n\n  try {\n    execSync(\n      `osascript -e \"set volume output volume ${Math.max(0, Math.min(100, volume))}\"`,\n    )\n    return true\n  } catch (error) {\n    log.error('Failed to set system volume:', error)\n    return false\n  }\n}\n\n/**\n * Mutes system audio and stores the previous volume\n */\nexport function muteSystemAudio(): boolean {\n  if (os.platform() !== 'darwin') {\n    log.warn('System audio control is only supported on macOS')\n    return false\n  }\n\n  try {\n    // Store current volume before muting\n    previousVolume = getSystemVolume()\n    if (previousVolume !== null) {\n      console.log(`Muting system audio. Previous volume: ${previousVolume}`)\n      return setSystemVolume(0)\n    }\n    return false\n  } catch (error) {\n    log.error('Failed to mute system audio:', error)\n    return false\n  }\n}\n\n/**\n * Unmutes system audio and restores the previous volume\n */\nexport function unmuteSystemAudio(): boolean {\n  if (os.platform() !== 'darwin') {\n    log.warn('System audio control is only supported on macOS')\n    return false\n  }\n\n  try {\n    if (previousVolume !== null) {\n      console.log(`Unmuting system audio. Restoring volume: ${previousVolume}`)\n      const success = setSystemVolume(previousVolume)\n      previousVolume = null // Clear stored volume\n      return success\n    } else {\n      log.warn('No previous volume stored, cannot unmute')\n      return false\n    }\n  } catch (error) {\n    log.error('Failed to unmute system audio:', error)\n    return false\n  }\n}\n"
  },
  {
    "path": "lib/media/text-writer.ts",
    "content": "import { execFile } from 'child_process'\nimport { platform, arch } from 'os'\nimport { getNativeBinaryPath } from './native-interface'\n\ninterface TextWriterOptions {\n  delay: number // Delay before typing (milliseconds)\n  charDelay: number // Delay between characters (milliseconds)\n}\n\nconst nativeModuleName = 'text-writer'\n\nexport function setFocusedText(\n  text: string,\n  options: TextWriterOptions = { delay: 0, charDelay: 0 },\n): Promise<boolean> {\n  return new Promise(resolve => {\n    const binaryPath = getNativeBinaryPath(nativeModuleName)\n    if (!binaryPath) {\n      console.error(\n        `Cannot determine ${nativeModuleName} binary path for platform ${platform()} and arch ${arch()}`,\n      )\n      return resolve(false)\n    }\n\n    const args: string[] = []\n\n    // Add optional arguments\n    if (options.delay !== undefined) {\n      args.push('--delay', options.delay.toString())\n    }\n    if (options.charDelay !== undefined) {\n      args.push('--char-delay', options.charDelay.toString())\n    }\n\n    // Add the text as the final argument with -- separator to prevent flag parsing\n    args.push('--', text)\n\n    execFile(binaryPath, args, (err, _stdout, stderr) => {\n      if (err) {\n        console.error('text-writer error:', stderr)\n        return resolve(false)\n      }\n      resolve(true)\n    })\n  })\n}\n"
  },
  {
    "path": "lib/preload/api.test.ts",
    "content": "import { describe, test, expect, beforeEach, mock } from 'bun:test'\n\n// Mock electron modules before importing api\nconst mockIpcRenderer = {\n  send: mock(),\n  on: mock(),\n  once: mock(),\n  invoke: mock(),\n  removeListener: mock(),\n  removeAllListeners: mock(),\n  sendSync: mock(),\n}\n\nmock.module('electron', () => ({\n  ipcRenderer: mockIpcRenderer,\n  IpcRendererEvent: class MockIpcRendererEvent {},\n}))\n\n// Import the API after mocking\nimport api from './api'\n\ndescribe('Preload API Critical Behavior Tests', () => {\n  beforeEach(() => {\n    // Reset all mocks\n    Object.values(mockIpcRenderer).forEach(mockFn => {\n      if (typeof mockFn.mockClear === 'function') {\n        mockFn.mockClear()\n      }\n    })\n  })\n\n  describe('Event Listener Cleanup', () => {\n    test('should clean up event listeners to prevent memory leaks', () => {\n      const callback = mock()\n      const cleanup = api.on('test-event', callback)\n\n      // Verify cleanup function works\n      cleanup()\n      expect(mockIpcRenderer.removeListener).toHaveBeenCalledWith(\n        'test-event',\n        expect.any(Function),\n      )\n    })\n\n    test('should unwrap IPC events before calling callbacks', () => {\n      const callback = mock()\n      api.on('test-event', callback)\n\n      // Simulate IPC event with extra event parameter\n      const registeredHandler = mockIpcRenderer.on.mock.calls[0][1]\n      const mockEvent = { sender: 'mock' }\n      registeredHandler(mockEvent, 'actual-data')\n\n      // Callback should receive only the data, not the event\n      expect(callback).toHaveBeenCalledWith('actual-data')\n    })\n  })\n\n  describe('Error Propagation', () => {\n    test('should propagate IPC errors correctly', async () => {\n      const error = new Error('IPC communication failed')\n      mockIpcRenderer.invoke.mockRejectedValueOnce(error)\n\n      expect(api.invoke('failing-channel')).rejects.toThrow(\n        'IPC communication failed',\n      )\n    })\n  })\n\n  describe('Parameter Transformations', () => {\n    test('should transform notes.updateContent parameters correctly', async () => {\n      await api.notes.updateContent('note-123', 'Updated content')\n\n      expect(mockIpcRenderer.invoke).toHaveBeenCalledWith(\n        'notes:update-content',\n        { id: 'note-123', content: 'Updated content' },\n      )\n    })\n\n    test('should transform dictionary.update parameters correctly', async () => {\n      await api.dictionary.update('dict-456', 'example', 'ig-zam-pul')\n\n      expect(mockIpcRenderer.invoke).toHaveBeenCalledWith('dictionary:update', {\n        id: 'dict-456',\n        word: 'example',\n        pronunciation: 'ig-zam-pul',\n      })\n    })\n\n    test('should handle null pronunciation in dictionary.update', async () => {\n      await api.dictionary.update('dict-789', 'test', null)\n\n      expect(mockIpcRenderer.invoke).toHaveBeenCalledWith('dictionary:update', {\n        id: 'dict-789',\n        word: 'test',\n        pronunciation: null,\n      })\n    })\n\n    test('should transform notifyLoginSuccess parameters correctly', async () => {\n      const profile = { id: 'user123', email: 'test@example.com' }\n      await api.notifyLoginSuccess(profile, 'id-token', 'access-token')\n\n      expect(mockIpcRenderer.invoke).toHaveBeenCalledWith(\n        'notify-login-success',\n        { profile, idToken: 'id-token', accessToken: 'access-token' },\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "lib/preload/api.ts",
    "content": "import { IpcRendererEvent, ipcRenderer } from 'electron'\nimport { AdvancedSettings } from '../main/store'\nimport { DbResult } from '../main/sqlite/repo'\nimport { DictionaryItem, UserMetadata } from '../main/sqlite/models'\n\nconst api = {\n  /**\n   * Sends a one-way message to the main process.\n   * @param channel The channel name to send the message on.\n   * @param data The data to send.\n   */\n  send: (channel: string, ...args: any[]) => {\n    ipcRenderer.send(channel, ...args)\n  },\n  /**\n   * Subscribe to an event, and return a cleanup function.\n   * @param channel The event channel to subscribe to.\n   * @param callback The callback to execute when the event is triggered.\n   * @returns A cleanup function to unsubscribe.\n   */\n  on: (channel: string, callback: (...args: any[]) => void): (() => void) => {\n    const handler = (_event: IpcRendererEvent, ...args: any[]) => {\n      callback(...args)\n    }\n    ipcRenderer.on(channel, handler)\n    return () => {\n      ipcRenderer.removeListener(channel, handler)\n    }\n  },\n  receive: (channel: string, func: (...args: any[]) => void) => {\n    ipcRenderer.on(channel, (_, ...args) => func(...args))\n  },\n  invoke: (channel: string, ...args: any[]) => {\n    return ipcRenderer.invoke(channel, ...args)\n  },\n  removeAllListeners: (channel: string) =>\n    ipcRenderer.removeAllListeners(channel),\n  // Key listener methods\n  startKeyListener: () => ipcRenderer.invoke('start-key-listener-service'),\n  stopKeyListener: () => ipcRenderer.invoke('stop-key-listener'),\n  registerHotkeys: () => ipcRenderer.invoke('register-hotkeys'),\n  startNativeRecording: () => ipcRenderer.invoke('start-native-recording'),\n  stopNativeRecording: () => ipcRenderer.invoke('stop-native-recording'),\n  getNativeAudioDevices: () => ipcRenderer.invoke('get-native-audio-devices'),\n  onVolumeUpdate: (callback: (volume: number) => void) => {\n    const handler = (_: any, volume: number) => callback(volume)\n    ipcRenderer.on('volume-update', handler)\n    return () => ipcRenderer.removeListener('volume-update', handler)\n  },\n  blockKeys: (keys: string[]) => ipcRenderer.invoke('block-keys', keys),\n  unblockKey: (key: string) => ipcRenderer.invoke('unblock-key', key),\n  getBlockedKeys: () => ipcRenderer.invoke('get-blocked-keys'),\n  onKeyEvent: (callback: (event: any) => void) => {\n    const handler = (_: any, event: any) => callback(event)\n    ipcRenderer.on('key-event', handler)\n    return () => ipcRenderer.removeListener('key-event', handler)\n  },\n  // Auth methods\n  generateNewAuthState: () => ipcRenderer.invoke('generate-new-auth-state'),\n  exchangeAuthCode: (data: any) =>\n    ipcRenderer.invoke('exchange-auth-code', data),\n  logout: () => ipcRenderer.invoke('logout'),\n  // Pill window mouse event control\n  setPillMouseEvents: (ignore: boolean, options?: { forward?: boolean }) =>\n    ipcRenderer.invoke('pill-set-mouse-events', ignore, options),\n  // All other IPC calls that the UI makes\n  // These now just pass through, as the main process handles the window context\n  'init-window': () => ipcRenderer.invoke('init-window'),\n  'is-window-minimizable': () => ipcRenderer.invoke('is-window-minimizable'),\n  'is-window-maximizable': () => ipcRenderer.invoke('is-window-maximizable'),\n  'window-minimize': () => ipcRenderer.invoke('window-minimize'),\n  'window-maximize': () => ipcRenderer.invoke('window-maximize'),\n  'window-close': () => ipcRenderer.invoke('window-close'),\n  'window-maximize-toggle': () => ipcRenderer.invoke('window-maximize-toggle'),\n  'web-undo': () => ipcRenderer.invoke('web-undo'),\n  'web-redo': () => ipcRenderer.invoke('web-redo'),\n  'web-cut': () => ipcRenderer.invoke('web-cut'),\n  'web-copy': () => ipcRenderer.invoke('web-copy'),\n  'web-paste': () => ipcRenderer.invoke('web-paste'),\n  'web-delete': () => ipcRenderer.invoke('web-delete'),\n  'web-select-all': () => ipcRenderer.invoke('web-select-all'),\n  'web-reload': () => ipcRenderer.invoke('web-reload'),\n  'web-force-reload': () => ipcRenderer.invoke('web-force-reload'),\n  'web-toggle-devtools': () => ipcRenderer.invoke('web-toggle-devtools'),\n  'web-actual-size': () => ipcRenderer.invoke('web-actual-size'),\n  'web-zoom-in': () => ipcRenderer.invoke('web-zoom-in'),\n  'web-zoom-out': () => ipcRenderer.invoke('web-zoom-out'),\n  'web-toggle-fullscreen': () => ipcRenderer.invoke('web-toggle-fullscreen'),\n  'web-open-url': (url: string) => ipcRenderer.invoke('web-open-url', url),\n  'check-accessibility-permission': (prompt: boolean) =>\n    ipcRenderer.invoke('check-accessibility-permission', prompt),\n  'check-microphone-permission': (prompt: boolean) =>\n    ipcRenderer.invoke('check-microphone-permission', prompt),\n  'start-native-recording': () => ipcRenderer.send('start-native-recording'),\n  'stop-native-recording': () => ipcRenderer.send('stop-native-recording'),\n  dev: {\n    revertLastMigration: () => ipcRenderer.invoke('dev:revert-last-migration'),\n    wipeDatabase: () => ipcRenderer.invoke('dev:wipe-database'),\n    checkSchema: () => ipcRenderer.invoke('debug:check-schema'),\n  },\n  notes: {\n    getAll: () => ipcRenderer.invoke('notes:get-all'),\n    add: (note: any) => ipcRenderer.invoke('notes:add', note),\n    updateContent: (id: string, content: string) =>\n      ipcRenderer.invoke('notes:update-content', { id, content }),\n    delete: (id: string) => ipcRenderer.invoke('notes:delete', id),\n  },\n  dictionary: {\n    getAll: () => ipcRenderer.invoke('dictionary:get-all'),\n    add: (item: any): Promise<DbResult<DictionaryItem>> =>\n      ipcRenderer.invoke('dictionary:add', item),\n    update: (\n      id: string,\n      word: string,\n      pronunciation: string | null,\n    ): Promise<DbResult<void>> =>\n      ipcRenderer.invoke('dictionary:update', { id, word, pronunciation }),\n    delete: (id: string) => ipcRenderer.invoke('dictionary:delete', id),\n  },\n  userMetadata: {\n    get: (): Promise<UserMetadata | null> =>\n      ipcRenderer.invoke('user-metadata:get'),\n    upsert: (metadata: UserMetadata): Promise<void> =>\n      ipcRenderer.invoke('user-metadata:upsert', metadata),\n    update: (\n      updates: Partial<Omit<UserMetadata, 'id' | 'user_id' | 'created_at'>>,\n    ): Promise<void> => ipcRenderer.invoke('user-metadata:update', updates),\n  },\n  interactions: {\n    getAll: () => ipcRenderer.invoke('interactions:get-all'),\n    getById: (id: string) => ipcRenderer.invoke('interactions:get-by-id', id),\n\n    delete: (id: string) => ipcRenderer.invoke('interactions:delete', id),\n  },\n  trial: {\n    complete: () => ipcRenderer.invoke('trial:complete'),\n    startAfterOnboarding: () =>\n      ipcRenderer.invoke('start-trial-after-onboarding'),\n  },\n  billing: {\n    createCheckoutSession: () =>\n      ipcRenderer.invoke('billing:create-checkout-session'),\n    confirmSession: (sessionId: string) =>\n      ipcRenderer.invoke('billing:confirm-session', { sessionId }),\n    status: () => ipcRenderer.invoke('billing:status'),\n    cancelSubscription: () => ipcRenderer.invoke('billing:cancel-subscription'),\n    reactivateSubscription: () =>\n      ipcRenderer.invoke('billing:reactivate-subscription'),\n  },\n  openMailto: (email: string) => ipcRenderer.invoke('open-mailto', email),\n  loginItem: {\n    setSettings: (enabled: boolean) =>\n      ipcRenderer.invoke('set-login-item-settings', enabled),\n    getSettings: () => ipcRenderer.invoke('get-login-item-settings'),\n  },\n  dock: {\n    setVisibility: (visible: boolean) =>\n      ipcRenderer.invoke('set-dock-visibility', visible),\n    getVisibility: () => ipcRenderer.invoke('get-dock-visibility'),\n  },\n  // Send settings updates to pill window\n  notifySettingsUpdate: (settings: any) =>\n    ipcRenderer.send('settings-update', settings),\n\n  // Send onboarding updates to pill window\n  notifyOnboardingUpdate: (onboarding: any) =>\n    ipcRenderer.send('onboarding-update', onboarding),\n\n  // Send user auth updates to pill window\n  notifyUserAuthUpdate: (authUser: any) =>\n    ipcRenderer.send('user-auth-update', authUser),\n\n  // Analytics device ID methods\n  'analytics:get-device-id': () =>\n    ipcRenderer.invoke('analytics:get-device-id'),\n  'analytics:resolve-install-token': () =>\n    ipcRenderer.invoke('analytics:resolve-install-token'),\n\n  // Onboarding state for current user\n  getOnboardingState: () => ipcRenderer.invoke('get-onboarding-state'),\n\n  notifyLoginSuccess: (\n    profile: any,\n    idToken: string | null,\n    accessToken: string | null,\n  ) => {\n    return ipcRenderer.invoke('notify-login-success', {\n      profile,\n      idToken,\n      accessToken,\n    })\n  },\n\n  // Delete user data from both local and server databases\n  deleteUserData: () => {\n    return ipcRenderer.invoke('delete-user-data')\n  },\n\n  updateAdvancedSettings: (advancedSettings: AdvancedSettings) => {\n    return ipcRenderer.invoke('update-advanced-settings', advancedSettings)\n  },\n\n  // Check if the local server is healthy and accessible\n  checkServerHealth: () => {\n    return ipcRenderer.invoke('check-server-health')\n  },\n\n  updater: {\n    onUpdateAvailable: callback => ipcRenderer.on('update-available', callback),\n    onUpdateDownloaded: callback =>\n      ipcRenderer.on('update-downloaded', callback),\n    installUpdate: () => ipcRenderer.send('install-update'),\n    getUpdateStatus: () => ipcRenderer.invoke('get-update-status'),\n  },\n\n  // Platform info\n  getPlatform: () => ipcRenderer.invoke('get-platform'),\n\n  // Selected Text Reader\n  selectedText: {\n    get: (options?: any) => ipcRenderer.invoke('get-selected-text', options),\n    getString: (maxLength?: number) =>\n      ipcRenderer.invoke('get-selected-text-string', maxLength),\n    hasSelected: () => ipcRenderer.invoke('has-selected-text'),\n  },\n\n  // Logs management\n  logs: {\n    download: () => ipcRenderer.invoke('logs:download'),\n    clear: () => ipcRenderer.invoke('logs:clear'),\n  },\n}\n\nexport default api\n"
  },
  {
    "path": "lib/preload/index.d.ts",
    "content": "import { ElectronAPI } from '@electron-toolkit/preload'\nimport type api from './api'\ntype TrialStatus = {\n  success: boolean\n  trialDays: number\n  trialStartAt: string | null\n  daysLeft: number\n  isTrialActive: boolean\n  hasCompletedTrial: boolean\n  error?: string\n  status?: number\n}\n\ninterface KeyEvent {\n  type: 'keydown' | 'keyup'\n  key: string\n  timestamp: string\n  raw_code: number\n}\n\ninterface StoreAPI {\n  get(key: string): any\n  set(property: string, val: any): void\n}\n\ninterface UpdaterAPI {\n  onUpdateAvailable: (callback: () => void) => void\n  onUpdateDownloaded: (callback: () => void) => void\n  installUpdate: () => void\n}\n\ninterface SelectedTextOptions {\n  format?: 'json' | 'text'\n  maxLength?: number\n}\n\ninterface SelectedTextResult {\n  success: boolean\n  text: string | null\n  error: string | null\n  length: number\n}\n\ninterface SelectedTextAPI {\n  get: (options?: SelectedTextOptions) => Promise<SelectedTextResult>\n  getString: (maxLength?: number) => Promise<string | null>\n  hasSelected: () => Promise<boolean>\n}\n\ndeclare global {\n  interface Window {\n    electron: ElectronAPI & {\n      store: StoreAPI\n    }\n    api: typeof api & {\n      updater: UpdaterAPI\n      startKeyListener: () => Promise<boolean>\n      stopKeyListener: () => Promise<boolean>\n      startNativeRecording: (deviceId: string) => Promise<void>\n      stopNativeRecording: () => Promise<void>\n      blockKeys: (keys: string[]) => Promise<void>\n      unblockKey: (key: string) => Promise<void>\n      getBlockedKeys: () => Promise<void>\n      onKeyEvent: (callback: (event: KeyEvent) => void) => void\n      send: (channel: string, data: any) => void\n      on: (channel: string, callback: (...args: any[]) => void) => () => void\n      setPillMouseEvents: (\n        ignore: boolean,\n        options?: { forward?: boolean },\n      ) => Promise<void>\n      generateNewAuthState: () => Promise<any>\n      exchangeAuthCode: (data: any) => Promise<any>\n      logout: () => Promise<void>\n      notes: {\n        getAll: () => Promise<Note[]>\n        add: (note: any) => Promise<Note>\n        updateContent: (id: string, content: string) => Promise<void>\n        delete: (id: string) => Promise<void>\n      }\n      dictionary: {\n        getAll: () => Promise<any[]>\n        add: (item: any) => Promise<any>\n        update: (\n          id: string,\n          word: string,\n          pronunciation: string | null,\n        ) => Promise<void>\n        delete: (id: string) => Promise<void>\n      }\n      interactions: {\n        getAll: () => Promise<any[]>\n        getById: (id: string) => Promise<any>\n        delete: (id: string) => Promise<void>\n      }\n      loginItem: {\n        setSettings: (enabled: boolean) => Promise<void>\n        getSettings: () => Promise<Electron.LoginItemSettings>\n      }\n      dock: {\n        setVisibility: (visible: boolean) => Promise<void>\n        getVisibility: () => Promise<{ isVisible: boolean }>\n      }\n      notifySettingsUpdate: (settings: any) => void\n      notifyOnboardingUpdate: (onboarding: any) => void\n      notifyUserAuthUpdate: (authUser: any) => void\n\n      getOnboardingState: () => Promise<{\n        onboardingStep?: number\n        onboardingCompleted?: boolean\n      } | null>\n\n      // Analytics device ID methods\n      'analytics:get-device-id': () => Promise<string | undefined>\n      'analytics:resolve-install-token': () => Promise<{\n        success: boolean\n        websiteDistinctId?: string | null\n        error?: string\n        status?: number\n      }>\n\n      notifyLoginSuccess: (\n        profile: any,\n        idToken: string | null,\n        accessToken: string | null,\n      ) => Promise<void>\n      trial: {\n        start: () => Promise<TrialStatus>\n        complete: () => Promise<TrialStatus>\n      }\n      billing: {\n        createCheckoutSession: () => Promise<{\n          success: boolean\n          url?: string\n          error?: string\n          status?: number\n        }>\n        confirmSession: (sessionId: string) => Promise<{\n          success: boolean\n          pro_status?: 'active_pro' | 'free_trial' | 'none'\n          subscriptionStartAt?: string\n          error?: string\n          status?: number\n        }>\n        status: () => Promise<{\n          success: boolean\n          pro_status: 'active_pro' | 'free_trial' | 'none'\n          subscriptionStartAt?: string\n          trial?: {\n            trialDays: number\n            trialStartAt: string | null\n            daysLeft: number\n            isTrialActive: boolean\n            hasCompletedTrial: boolean\n          }\n          error?: string\n          status?: number\n        }>\n      }\n      deleteUserData: () => Promise<void>\n      selectedText: SelectedTextAPI\n      logs: {\n        download: () => Promise<{\n          success: boolean\n          path?: string\n          error?: string\n        }>\n        clear: () => Promise<{\n          success: boolean\n          error?: string\n        }>\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/preload/preload.ts",
    "content": "import { contextBridge, ipcRenderer } from 'electron'\nimport { electronAPI } from '@electron-toolkit/preload'\nimport api from './api'\nimport * as log from 'electron-log/renderer'\n\n// Override the console object in the renderer process\nObject.assign(console, log.functions)\n\n// Use `contextBridge` APIs to expose Electron APIs to\n// renderer only if context isolation is enabled, otherwise\n// just add to the DOM global.\nif (process.contextIsolated) {\n  try {\n    contextBridge.exposeInMainWorld('electron', {\n      ...electronAPI,\n      store: {\n        get(key) {\n          return ipcRenderer.sendSync('electron-store-get', key)\n        },\n        set(property, val) {\n          ipcRenderer.send('electron-store-set', property, val)\n        },\n      },\n    })\n    contextBridge.exposeInMainWorld('api', api)\n  } catch (error) {\n    console.error(error)\n  }\n} else {\n  // In non-context-isolated environments, we need to manually\n  // construct the same object that contextBridge would create.\n  window.electron = {\n    ...electronAPI,\n    store: {\n      get(key) {\n        return ipcRenderer.sendSync('electron-store-get', key)\n      },\n      set(property, val) {\n        ipcRenderer.send('electron-store-set', property, val)\n      },\n    },\n  }\n  window.api = api\n}\n"
  },
  {
    "path": "lib/protocol/index.ts",
    "content": "import { app, BrowserWindow } from 'electron'\nimport path from 'path'\nimport { mainWindow } from '../main/app'\nimport { ITO_ENV } from '../main/env'\n\n// Protocol handling for deep links\nconst PROTOCOL = ITO_ENV === 'prod' ? 'ito' : `ito-dev`\n\n// Handle protocol URL\nfunction handleProtocolUrl(url: string) {\n  try {\n    const urlObj = new URL(url)\n\n    if (\n      urlObj.protocol === `${PROTOCOL}:` &&\n      urlObj.hostname === 'auth' &&\n      urlObj.pathname === '/callback'\n    ) {\n      const authCode = urlObj.searchParams.get('code')\n      const state = urlObj.searchParams.get('state')\n\n      if (authCode && state) {\n        // Find the main window (not the pill window) and send the auth code\n        if (\n          mainWindow &&\n          !mainWindow.isDestroyed() &&\n          !mainWindow.webContents.isDestroyed()\n        ) {\n          const sendToRenderer = () => {\n            if (\n              mainWindow &&\n              !mainWindow.isDestroyed() &&\n              !mainWindow.webContents.isDestroyed()\n            ) {\n              mainWindow.webContents.send('auth-code-received', authCode, state)\n            }\n          }\n\n          if (mainWindow.webContents.isLoadingMainFrame()) {\n            mainWindow.webContents.once('did-finish-load', () => {\n              sendToRenderer()\n            })\n          } else {\n            sendToRenderer()\n          }\n\n          // Focus and show the window with more aggressive methods\n          mainWindow.show()\n          mainWindow.focus()\n          mainWindow.setAlwaysOnTop(true)\n          mainWindow.setAlwaysOnTop(false)\n\n          // On macOS, use additional methods to force focus\n          if (process.platform === 'darwin') {\n            mainWindow.moveTop()\n            app.focus({ steal: true })\n            app.dock?.show()\n          }\n        } else {\n          console.error('No main window found to send auth code to')\n        }\n      } else {\n        console.warn('No auth code found in protocol URL')\n      }\n    } else if (\n      urlObj.protocol === `${PROTOCOL}:` &&\n      urlObj.hostname === 'billing'\n    ) {\n      const sendToRenderer = (channel: string, ...args: any[]) => {\n        if (\n          mainWindow &&\n          !mainWindow.isDestroyed() &&\n          !mainWindow.webContents.isDestroyed()\n        ) {\n          const doSend = () => {\n            if (\n              mainWindow &&\n              !mainWindow.isDestroyed() &&\n              !mainWindow.webContents.isDestroyed()\n            ) {\n              mainWindow.webContents.send(channel, ...args)\n            }\n          }\n          if (mainWindow.webContents.isLoadingMainFrame()) {\n            mainWindow.webContents.once('did-finish-load', () => doSend())\n          } else {\n            doSend()\n          }\n\n          // Bring the app to front\n          mainWindow.show()\n          mainWindow.focus()\n          mainWindow.setAlwaysOnTop(true)\n          mainWindow.setAlwaysOnTop(false)\n          if (process.platform === 'darwin') {\n            mainWindow.moveTop()\n            app.focus({ steal: true })\n            app.dock?.show()\n          }\n        }\n      }\n\n      if (urlObj.pathname === '/success') {\n        const sessionId = urlObj.searchParams.get('session_id') || ''\n        sendToRenderer('billing-session-completed', sessionId)\n      } else if (urlObj.pathname === '/cancel') {\n        sendToRenderer('billing-session-cancelled')\n      }\n    } else {\n      console.warn('Protocol URL does not match expected format')\n      console.warn(\n        `Expected: ${PROTOCOL}: with hostname 'auth' and pathname '/callback'`,\n      )\n      console.warn(\n        `Received: ${urlObj.protocol} with hostname '${urlObj.hostname}' and pathname '${urlObj.pathname}'`,\n      )\n    }\n  } catch (error) {\n    console.error('Error parsing protocol URL:', error)\n  }\n}\n\n// Setup protocol handling\nexport function setupProtocolHandling(): void {\n  // Register protocol handler\n  if (process.defaultApp || !app.isPackaged) {\n    const appPath = app.getAppPath()\n    const registered = app.setAsDefaultProtocolClient(\n      PROTOCOL,\n      process.execPath,\n      [appPath],\n    )\n    if (!registered) {\n      // Fallback to using argv[1] when available\n      const target = process.argv[1] ? path.resolve(process.argv[1]) : undefined\n      if (target) {\n        app.setAsDefaultProtocolClient(PROTOCOL, process.execPath, [target])\n      }\n    }\n  } else {\n    if (!app.isDefaultProtocolClient(PROTOCOL)) {\n      app.setAsDefaultProtocolClient(PROTOCOL)\n    }\n  }\n\n  // Handle protocol on Windows\n  const gotTheLock = app.requestSingleInstanceLock()\n\n  if (!gotTheLock) {\n    app.quit()\n    return\n  }\n\n  app.on('second-instance', (_event, commandLine, _workingDirectory) => {\n    // Someone tried to run a second instance, we should focus our window instead\n    const mainWindow = BrowserWindow.getAllWindows().find(\n      win => !win.isDestroyed(),\n    )\n    if (mainWindow) {\n      if (mainWindow.isMinimized()) mainWindow.restore()\n      mainWindow.focus()\n    }\n\n    // Handle protocol URL on Windows\n    const url = commandLine.find(arg => arg.startsWith(`${PROTOCOL}://`))\n    if (url) {\n      handleProtocolUrl(url)\n    }\n  })\n\n  // Handle protocol on macOS\n  app.on('open-url', (event, url) => {\n    event.preventDefault()\n    handleProtocolUrl(url)\n  })\n}\n\n// Export the protocol name for use in other modules if needed\nexport { PROTOCOL }\n\n// Process deep link if the app was started via protocol (first instance)\nexport function processStartupProtocolUrl(): void {\n  const urlArg = process.argv.find(arg => arg.startsWith(`${PROTOCOL}://`))\n  if (urlArg) {\n    handleProtocolUrl(urlArg)\n  }\n}\n"
  },
  {
    "path": "lib/types/cursorContext.ts",
    "content": "/**\n * Cursor Context Types\n *\n * Defines types for retrieving text surrounding the cursor position\n * using accessibility APIs (NSAccessibility on macOS, UIAutomation on Windows)\n */\n\n/**\n * Position of the cursor within a text field\n */\nexport interface CursorPosition {\n  /** Character offset from start of text */\n  offset: number\n  /** Line number (0-indexed) */\n  line?: number\n  /** Column number (0-indexed) */\n  column?: number\n}\n\n/**\n * Range of text within a text field\n */\nexport interface TextRange {\n  /** Start position (character offset) */\n  start: number\n  /** End position (character offset) */\n  end: number\n  /** Length of the range */\n  length: number\n}\n\n/**\n * Text content surrounding the cursor with metadata\n */\nexport interface CursorContext {\n  /** Text before the cursor */\n  textBefore: string\n  /** Text after the cursor */\n  textAfter: string\n  /** Currently selected/highlighted text, if any */\n  selectedText: string\n  /** Current cursor position */\n  cursorPosition: CursorPosition\n  /** Selection range, if text is selected */\n  selectionRange?: TextRange\n  /** Whether the text was truncated due to length limits */\n  truncated: boolean\n  /** Total character count in the text field */\n  totalLength: number\n  /** Timestamp when context was captured */\n  timestamp: string\n}\n\n/**\n * Complete cursor context result including success/error status\n */\nexport interface CursorContextResult {\n  success: boolean\n  context?: CursorContext\n  error?: string\n  /** Method used to retrieve context (for debugging/telemetry) */\n  method: 'accessibility' | 'ocr' | 'clipboard' | 'keyboard'\n}\n\n/**\n * Options for retrieving cursor context\n */\nexport interface CursorContextOptions {\n  /**\n   * Maximum characters to retrieve before cursor\n   */\n  maxCharsBefore: number\n\n  /**\n   * Maximum characters to retrieve after cursor\n   */\n  maxCharsAfter: number\n\n  /**\n   * Timeout in milliseconds\n   */\n  timeout: number\n\n  /**\n   * Enable debug logging to stderr\n   */\n  debug: boolean\n}\n"
  },
  {
    "path": "lib/types/ipc.ts",
    "content": "import { ItoMode } from '@/app/generated/ito_pb'\n\n// IPC Event Constants\nexport const IPC_EVENTS = {\n  RECORDING_STATE_UPDATE: 'recording-state-update',\n  PROCESSING_STATE_UPDATE: 'processing-state-update',\n  VOLUME_UPDATE: 'volume-update',\n  FORCE_DEVICE_LIST_RELOAD: 'force-device-list-reload',\n  SETTINGS_UPDATE: 'settings-update',\n  ONBOARDING_UPDATE: 'onboarding-update',\n  USER_AUTH_UPDATE: 'user-auth-update',\n} as const\n\n// IPC Payload Types\nexport interface RecordingStatePayload {\n  isRecording: boolean\n  mode?: ItoMode\n}\n\nexport interface ProcessingStatePayload {\n  isProcessing: boolean\n  mode?: ItoMode\n}\n\nexport interface VolumeUpdatePayload {\n  volume: number\n}\n\n// Generic IPC Response Types\nexport type IpcResult<T> =\n  | { success: true; data: T }\n  | { success: false; error: string; errorType?: string }\n\nexport type IpcResponse<T> = Promise<IpcResult<T>>\n"
  },
  {
    "path": "lib/types/keyboard.ts",
    "content": "// Shared keyboard key types between keyboard.ts and UI components\n\n// Map of raw key names to their normalized representations\nexport const keyNameMap: Record<string, KeyName> = {\n  MetaLeft: 'command-left',\n  MetaRight: 'command-right',\n  ControlLeft: 'control-left',\n  ControlRight: 'control-right',\n  Alt: 'option-left',\n  AltGr: 'option-right',\n  ShiftLeft: 'shift-left',\n  ShiftRight: 'shift-right',\n  Function: 'fn',\n  'Unknown(179)': 'fn_fast',\n  KeyA: 'a',\n  KeyB: 'b',\n  KeyC: 'c',\n  KeyD: 'd',\n  KeyE: 'e',\n  KeyF: 'f',\n  KeyG: 'g',\n  KeyH: 'h',\n  KeyI: 'i',\n  KeyJ: 'j',\n  KeyK: 'k',\n  KeyL: 'l',\n  KeyM: 'm',\n  KeyN: 'n',\n  KeyO: 'o',\n  KeyP: 'p',\n  KeyQ: 'q',\n  KeyR: 'r',\n  KeyS: 's',\n  KeyT: 't',\n  KeyU: 'u',\n  KeyV: 'v',\n  KeyW: 'w',\n  KeyX: 'x',\n  KeyY: 'y',\n  KeyZ: 'z',\n  Digit1: '1',\n  Digit2: '2',\n  Digit3: '3',\n  Digit4: '4',\n  Digit5: '5',\n  Digit6: '6',\n  Digit7: '7',\n  Digit8: '8',\n  Digit9: '9',\n  Digit0: '0',\n  Space: 'space',\n  Enter: 'enter',\n  Escape: 'esc',\n  Backspace: 'backspace',\n  Tab: 'tab',\n  CapsLock: 'caps',\n  Delete: 'delete',\n  ArrowUp: '↑',\n  ArrowDown: '↓',\n  ArrowLeft: '←',\n  ArrowRight: '→',\n}\n\nexport type ModifierKey =\n  | 'command-left'\n  | 'command-right'\n  | 'control-left'\n  | 'control-right'\n  | 'option-left'\n  | 'option-right'\n  | 'shift-left'\n  | 'shift-right'\n  | 'fn'\n  | 'fn_fast'\n\nexport type RegularKey =\n  | 'a'\n  | 'b'\n  | 'c'\n  | 'd'\n  | 'e'\n  | 'f'\n  | 'g'\n  | 'h'\n  | 'i'\n  | 'j'\n  | 'k'\n  | 'l'\n  | 'm'\n  | 'n'\n  | 'o'\n  | 'p'\n  | 'q'\n  | 'r'\n  | 's'\n  | 't'\n  | 'u'\n  | 'v'\n  | 'w'\n  | 'x'\n  | 'y'\n  | 'z'\n  | '1'\n  | '2'\n  | '3'\n  | '4'\n  | '5'\n  | '6'\n  | '7'\n  | '8'\n  | '9'\n  | '0'\n  | 'space'\n  | 'enter'\n  | 'esc'\n  | 'backspace'\n  | 'tab'\n  | 'caps'\n  | 'delete'\n  | '↑'\n  | '↓'\n  | '←'\n  | '→'\n\nexport type KeyName = ModifierKey | RegularKey\n\n// Legacy key names for backward compatibility\nexport const legacyKeyMap: Record<string, string> = {\n  command: 'command-left', // Default to left for legacy\n  control: 'control-left',\n  option: 'option-left',\n  shift: 'shift-left',\n  alt: 'option-left', // Map alt to option-left for consistency\n}\n\n// Function to normalize legacy keys to new format\nexport function normalizeLegacyKey(key: string): KeyName {\n  return (legacyKeyMap[key] || key) as KeyName\n}\n\n// Platform-specific display information\nexport interface KeyDisplayInfo {\n  label: string\n  symbol?: string\n  isModifier: boolean\n  side?: 'left' | 'right'\n}\n\n// Helper function to get display info for a key\nexport function getKeyDisplayInfo(\n  keyName: KeyName,\n  platform: 'darwin' | 'win32' = 'darwin',\n): KeyDisplayInfo {\n  // Handle directional modifiers\n  const normalizedKey = normalizeLegacyKey(keyName)\n  if (normalizedKey.includes('-')) {\n    const [baseKey, side] = keyName.split('-') as [string, 'left' | 'right']\n\n    switch (baseKey) {\n      case 'command':\n        // macOS uses Command (⌘), Windows uses Win key (⊞)\n        if (platform === 'darwin') {\n          return {\n            label: 'cmd',\n            symbol: '⌘',\n            isModifier: true,\n            side,\n          }\n        } else {\n          return {\n            label: 'win',\n            symbol: '⊞',\n            isModifier: true,\n            side,\n          }\n        }\n      case 'control':\n        return {\n          label: 'ctrl',\n          symbol: platform === 'darwin' ? '⌃' : 'Ctrl',\n          isModifier: true,\n          side,\n        }\n      case 'shift':\n        return {\n          label: 'shift',\n          symbol: '⇧',\n          isModifier: true,\n          side,\n        }\n      case 'fn':\n      case 'option':\n        // Option key is Alt on Windows\n        if (platform === 'darwin') {\n          return {\n            label: 'option',\n            symbol: '⌥',\n            isModifier: true,\n            side,\n          }\n        } else {\n          return {\n            label: 'alt',\n            symbol: 'Alt',\n            isModifier: true,\n            side,\n          }\n        }\n    }\n  }\n\n  // Handle non-directional keys\n  switch (normalizedKey) {\n    case 'fn':\n      return {\n        label: 'fn',\n        isModifier: true,\n      }\n    default:\n      return {\n        label: keyName,\n        isModifier: false,\n      }\n  }\n}\n"
  },
  {
    "path": "lib/utils/applicationDetection.ts",
    "content": "import { getActiveWindow } from '../media/active-application'\n\nconst TERMINAL_APPS = new Set([\n  // macOS terminals\n  'terminal',\n  'iterm2',\n  'iterm',\n  'alacritty',\n  'kitty',\n  'hyper',\n  'warp',\n  'wezterm',\n  'tabby',\n  'rio',\n  'console',\n  'xterm',\n\n  // Windows terminals\n  'windows terminal',\n  'command prompt',\n  'powershell',\n  'windows powershell',\n  'git bash',\n  'msys2',\n  'cygwin',\n  'ubuntu', // WSL Ubuntu\n  'debian', // WSL Debian\n  'kali', // WSL Kali\n\n  // IDEs with integrated terminals (cross-platform)\n  'visual studio code',\n  'visual studio code - insiders',\n  'code',\n  'code - insiders',\n  'visual studio',\n  'visual studio 2022',\n  'visual studio 2019',\n  'intellij idea',\n  'intellij idea ultimate',\n  'intellij idea community edition',\n  'webstorm',\n  'pycharm',\n  'pycharm professional',\n  'pycharm community edition',\n  'clion',\n  'phpstorm',\n  'rubymine',\n  'goland',\n  'datagrip',\n  'rider',\n  'android studio',\n  'neovim',\n  'vim',\n  'emacs',\n\n  // Linux terminals\n  'gnome-terminal',\n  'konsole',\n  'xfce4-terminal',\n  'mate-terminal',\n  'lxterminal',\n  'terminator',\n  'tilix',\n  'guake',\n  'yakuake',\n])\n\nconst AXApiNotSupportedApps = new Set([\n  'visual studio code',\n  'visual studio code - insiders',\n  'code',\n  'code - insiders',\n  'visual studio',\n  'visual studio 2022',\n  'visual studio 2019',\n])\n\nexport async function canGetContextWithAccessibilityApis(): Promise<boolean> {\n  try {\n    const window = await getActiveWindow()\n    if (!window?.appName) {\n      return false // Default to disallowing context if we can't determine\n    }\n    const lowerAppName = window.appName.toLowerCase()\n    return !AXApiNotSupportedApps.has(lowerAppName)\n  } catch (error) {\n    console.error('Failed to get active window:', error)\n    return false // Default to not allowing context on error\n  }\n}\n\nexport function isTerminalApplication(appName: string): boolean {\n  const lowerAppName = appName.toLowerCase()\n  return TERMINAL_APPS.has(lowerAppName)\n}\n\nexport async function canGetContextFromCurrentApp(): Promise<boolean> {\n  try {\n    const window = await getActiveWindow()\n    if (!window?.appName) {\n      return false // Default to disallowing context if we can't determine\n    }\n    return !isTerminalApplication(window.appName)\n  } catch (error) {\n    console.error('Failed to get active window:', error)\n    return false // Default to not allowing context on error\n  }\n}\n"
  },
  {
    "path": "lib/utils/crossPlatform.ts",
    "content": "import { systemPreferences } from 'electron'\n\n/**\n * Cross-platform utility functions for handling OS-specific functionality\n */\n\n/**\n * Checks if the app has accessibility permissions\n * On macOS: Uses systemPreferences.isTrustedAccessibilityClient()\n * On Windows: Returns true (no accessibility permissions required)\n */\nexport function checkAccessibilityPermission(prompt: boolean = false): boolean {\n  if (process.platform === 'darwin') {\n    return systemPreferences.isTrustedAccessibilityClient(prompt)\n  }\n\n  // On Windows, accessibility permissions aren't required\n  return true\n}\n\n/**\n * Checks if the app has microphone permissions\n * Cross-platform implementation using systemPreferences\n */\nexport async function checkMicrophonePermission(\n  prompt: boolean = false,\n): Promise<boolean> {\n  if (process.platform === 'darwin') {\n    // macOS - use system preferences API\n    if (prompt) {\n      return systemPreferences.askForMediaAccess('microphone')\n    }\n    return systemPreferences.getMediaAccessStatus('microphone') === 'granted'\n  }\n\n  // Windows - microphone permissions are handled by the OS\n  // and don't require explicit permission checks in Electron apps\n  return true\n}\n"
  },
  {
    "path": "lib/utils/settings.test.ts",
    "content": "import { describe, expect, test } from 'bun:test'\nimport { resolveDefaultKeys } from './settings'\nimport { DEFAULT_ADVANCED_SETTINGS } from '../constants/generated-defaults'\n\nconst settings = {\n  asrProvider: 'asrProvider',\n  asrModel: 'asrModel',\n  asrPrompt: 'asrPrompt',\n  llmProvider: 'llmProvider',\n  llmModel: 'llmModel',\n  llmTemperature: 0.5,\n  transcriptionPrompt: 'transcriptionPrompt',\n  editingPrompt: 'editingPrompt',\n  noSpeechThreshold: 0.7,\n}\n\nconst defaults = {\n  asrProvider: 'defaultAsrProvider',\n  asrModel: 'defaultAsrModel',\n  asrPrompt: 'defaultAsrPrompt',\n  llmProvider: 'defaultLlmProvider',\n  llmModel: 'defaultLlmModel',\n  llmTemperature: 0.8,\n  transcriptionPrompt: 'defaultTranscriptionPrompt',\n  editingPrompt: 'defaultEditingPrompt',\n  noSpeechThreshold: 0.9,\n}\n\ndescribe('resolve default keys', () => {\n  test('should resolve null values correctly', () => {\n    const testSettings = {\n      ...settings,\n      asrProvider: null,\n      asrModel: null,\n    }\n    const result = resolveDefaultKeys(testSettings, defaults)\n    expect(result.asrProvider).toBe(defaults.asrProvider)\n    expect(result.asrModel).toBe(defaults.asrModel)\n    expect(result.asrPrompt).toBe(settings.asrPrompt)\n  })\n\n  test('if null but no default provided, should fallback to constants', () => {\n    const testSettings = {\n      ...settings,\n      asrProvider: null,\n      asrModel: null,\n      llmProvider: null,\n    }\n    const result = resolveDefaultKeys(testSettings)\n    expect(result.asrProvider).toBe(DEFAULT_ADVANCED_SETTINGS.asrProvider)\n    expect(result.asrModel).toBe(DEFAULT_ADVANCED_SETTINGS.asrModel)\n    expect(result.llmProvider).toBe(DEFAULT_ADVANCED_SETTINGS.llmProvider)\n  })\n})\n"
  },
  {
    "path": "lib/utils/settings.ts",
    "content": "import { DEFAULT_ADVANCED_SETTINGS } from '../constants/generated-defaults.js'\nimport type { LlmSettings } from '@/app/store/useAdvancedSettingsStore'\n\nexport function resolveDefaultKeys(\n  llmSettings: LlmSettings,\n  defaults?: LlmSettings,\n): LlmSettings {\n  const resolved = { ...llmSettings }\n\n  for (const key in llmSettings) {\n    const typedKey = key as keyof LlmSettings\n    if (llmSettings[typedKey] === null) {\n      resolved[typedKey] = (defaults?.[typedKey] ??\n        DEFAULT_ADVANCED_SETTINGS[typedKey]) as any\n    }\n  }\n\n  return resolved\n}\n"
  },
  {
    "path": "lib/utils.ts",
    "content": "import { clsx, type ClassValue } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"
  },
  {
    "path": "lib/window/index.ts",
    "content": "export {\n  WindowContextProvider,\n  useWindowContext,\n} from '@/app/components/window/WindowContext'\nexport {\n  TitlebarContextProvider,\n  useTitlebarContext,\n} from '@/app/components/window/TitlebarContext'\n"
  },
  {
    "path": "lib/window/ipcDev.ts",
    "content": "import { ipcMain } from 'electron'\nimport { revertLastMigration, wipeDatabase } from '../main/sqlite/db'\n\nexport function registerDevIPC() {\n  ipcMain.handle('dev:revert-last-migration', async () => {\n    console.log('Received dev:revert-last-migration IPC call.')\n    try {\n      await revertLastMigration()\n      return { success: true }\n    } catch (error) {\n      console.error('Failed to revert last migration:', error)\n      return { success: false, error: (error as Error).message }\n    }\n  })\n\n  ipcMain.handle('dev:wipe-database', async () => {\n    console.log('Received dev:wipe-database IPC call.')\n    try {\n      await wipeDatabase()\n      return { success: true }\n    } catch (error) {\n      console.error('Failed to wipe database:', error)\n      return { success: false, error: (error as Error).message }\n    }\n  })\n}\n"
  },
  {
    "path": "lib/window/ipcEvents.test.ts",
    "content": "import { describe, test, expect, beforeEach, mock } from 'bun:test'\nimport { registerIPC } from './ipcEvents'\n\nconst mockIpcMain = (await import('electron')).ipcMain as any\nconst mockSystemPreferences = (await import('electron'))\n  .systemPreferences as any\nconst mockBrowserWindow = (await import('electron')).BrowserWindow as any\nlet mockGetCurrentUserId = (await import('../main/store'))\n  .getCurrentUserId as any\nconst mockNotesTable = (await import('../main/sqlite/repo')).NotesTable as any\nlet mockEnsureValidTokens = (await import('../auth/events'))\n  .ensureValidTokens as any\nconst mockElectronLog = (await import('electron-log')).default as any\n\ndescribe('IPC Events Critical Business Logic Tests', () => {\n  let registeredHandlers: Map<string, (...args: any[]) => any>\n\n  beforeEach(() => {\n    registeredHandlers = new Map()\n\n    const originalHandle = mockIpcMain.handle\n    mockIpcMain.handle = (\n      channel: string,\n      handler: (...args: any[]) => any,\n    ) => {\n      registeredHandlers.set(channel, handler)\n      return originalHandle(channel, handler)\n    }\n\n    registerIPC()\n  })\n\n  describe('Token Refresh Error Handling', () => {\n    test('should handle token refresh errors gracefully', async () => {\n      const handler = registeredHandlers.get('refresh-tokens')\n      const error = new Error('Token refresh failed')\n\n      const originalEnsureValidTokens = mockEnsureValidTokens\n      mockEnsureValidTokens = async () => {\n        throw error\n      }\n\n      expect(handler).toBeDefined()\n      const result = await handler!()\n\n      expect(result).toEqual({\n        success: false,\n        error: 'No refresh token available',\n      })\n\n      mockEnsureValidTokens = originalEnsureValidTokens\n    })\n  })\n\n  describe('Microphone Permission Logic', () => {\n    test('should handle microphone permission with prompt', async () => {\n      const handler = registeredHandlers.get('check-microphone-permission')\n\n      const originalAskForMediaAccess = mockSystemPreferences.askForMediaAccess\n      mockSystemPreferences.askForMediaAccess = async () => true\n\n      expect(handler).toBeDefined()\n      const result = await handler!({}, true)\n\n      expect(result).toBe(true)\n\n      mockSystemPreferences.askForMediaAccess = originalAskForMediaAccess\n    })\n\n    test('should handle microphone permission without prompt', async () => {\n      const handler = registeredHandlers.get('check-microphone-permission')\n\n      const originalGetMediaAccessStatus =\n        mockSystemPreferences.getMediaAccessStatus\n      mockSystemPreferences.getMediaAccessStatus = () => 'granted'\n\n      expect(handler).toBeDefined()\n      const result = await handler!({}, false)\n\n      expect(result).toBe(true)\n\n      mockSystemPreferences.getMediaAccessStatus = originalGetMediaAccessStatus\n    })\n  })\n\n  describe('Window State Logic', () => {\n    test('should handle window maximize toggle correctly', async () => {\n      const handler = registeredHandlers.get('window-maximize-toggle')\n\n      const originalFromWebContents = mockBrowserWindow.fromWebContents\n      const mockWindow = {\n        isMaximized: () => true,\n        unmaximize: () => {},\n        maximize: () => {},\n      }\n      mockBrowserWindow.fromWebContents = () => mockWindow\n\n      expect(handler).toBeDefined()\n      await handler!({ sender: 'mock' })\n\n      mockBrowserWindow.fromWebContents = originalFromWebContents\n    })\n\n    test('should return correct window initialization data', async () => {\n      const handler = registeredHandlers.get('init-window')\n      const mockWindow = {\n        getBounds: () => ({ width: 1200, height: 800 }),\n        isMinimizable: () => true,\n        isMaximizable: () => false,\n      }\n\n      const originalFromWebContents = mockBrowserWindow.fromWebContents\n      mockBrowserWindow.fromWebContents = () => mockWindow\n\n      expect(handler).toBeDefined()\n      const result = await handler!({ sender: 'mock' })\n\n      expect(result).toEqual({\n        width: 1200,\n        height: 800,\n        minimizable: true,\n        maximizable: false,\n        platform: process.platform,\n      })\n\n      mockBrowserWindow.fromWebContents = originalFromWebContents\n    })\n  })\n\n  describe('Data Transformation Logic', () => {\n    test('should inject user ID for database operations', async () => {\n      const handler = registeredHandlers.get('notes:get-all')\n      const mockNotes = [{ id: '1', content: 'Test note' }]\n\n      const originalFindAll = mockNotesTable.findAll\n      mockNotesTable.findAll = async () => mockNotes\n\n      expect(handler).toBeDefined()\n      const result = await handler!()\n\n      expect(result).toEqual(mockNotes)\n\n      mockNotesTable.findAll = originalFindAll\n    })\n\n    test('should handle missing user ID for data deletion', async () => {\n      const handler = registeredHandlers.get('delete-user-data')\n\n      // Mock electron-log to suppress the log output for this test\n      const originalError = mockElectronLog.error\n      mockElectronLog.error = () => {}\n\n      const originalGetCurrentUserId = mockGetCurrentUserId\n      mockGetCurrentUserId = () => null\n\n      expect(handler).toBeDefined()\n      const result = await handler!({})\n\n      expect(result).toBe(false)\n\n      // Restore original functions\n      mockGetCurrentUserId = originalGetCurrentUserId\n      mockElectronLog.error = originalError\n    })\n\n    test('update-advanced-settings should call the correct service', async () => {\n      const mockUpdateAdvancedSettings = mock(async (settings: any) => settings)\n      const grpcClient = {\n        updateAdvancedSettings: mockUpdateAdvancedSettings,\n      }\n\n      mock.module('../clients/grpcClient', () => ({\n        grpcClient,\n      }))\n\n      const handler = registeredHandlers.get('update-advanced-settings')\n      const mockSettings = { setting1: 'value1', setting2: 'value2' }\n\n      expect(handler).toBeDefined()\n      const result = await handler!({}, mockSettings)\n\n      expect(result).toEqual(mockSettings)\n      expect(mockUpdateAdvancedSettings).toHaveBeenCalledWith(mockSettings)\n    })\n  })\n})\n"
  },
  {
    "path": "lib/window/ipcEvents.ts",
    "content": "import { BrowserWindow, ipcMain, shell, app, dialog } from 'electron'\nimport log from 'electron-log'\nimport os from 'os'\nimport { exec } from 'child_process'\nimport fs from 'fs/promises'\nimport path from 'path'\nimport store, { getCurrentUserId } from '../main/store'\nimport { STORE_KEYS } from '../constants/store-keys'\nimport {\n  checkAccessibilityPermission,\n  checkMicrophonePermission,\n} from '../utils/crossPlatform'\nimport { getUpdateStatus, installUpdateNow } from '../main/autoUpdaterWrapper'\n\nimport {\n  startKeyListener,\n  KeyListenerProcess,\n  stopKeyListener,\n  registerAllHotkeys,\n} from '../media/keyboard'\nimport { getPillWindow, mainWindow } from '../main/app'\nimport {\n  generateNewAuthState,\n  exchangeAuthCode,\n  handleLogin,\n  handleLogout,\n  ensureValidTokens,\n} from '../auth/events'\nimport { KeyValueStore } from '../main/sqlite/repo'\nimport { machineId } from 'node-machine-id'\nimport { Auth0Config, Auth0Connections } from '../auth/config'\nimport {\n  NotesTable,\n  DictionaryTable,\n  InteractionsTable,\n  UserMetadataTable,\n} from '../main/sqlite/repo'\nimport { audioRecorderService } from '../media/audio'\nimport { voiceInputService } from '../main/voiceInputService'\nimport { itoSessionManager } from '../main/itoSessionManager'\nimport { ItoMode } from '@/app/generated/ito_pb'\nimport {\n  getSelectedText,\n  getSelectedTextString,\n  hasSelectedText,\n} from '../media/selected-text-reader'\nimport { IPC_EVENTS } from '../types/ipc'\nimport { itoHttpClient } from '../clients/itoHttpClient'\n\nconst handleIPC = (channel: string, handler: (...args: any[]) => any) => {\n  ipcMain.handle(channel, handler)\n}\n\n// This single function registers all IPC handlers for the application.\n// It should only be called once.\nexport function registerIPC() {\n  // Store\n  ipcMain.on('electron-store-get', (event, val) => {\n    event.returnValue = store.get(val)\n  })\n  ipcMain.on('electron-store-set', (_event, key, val) => {\n    store.set(key, val)\n  })\n\n  ipcMain.on('audio-devices-changed', () => {\n    console.log('[IPC] Audio devices changed, notifying windows.')\n    // Notify all windows to refresh their device lists in the UI.\n    if (\n      mainWindow &&\n      !mainWindow.isDestroyed() &&\n      !mainWindow.webContents.isDestroyed()\n    ) {\n      mainWindow.webContents.send(IPC_EVENTS.FORCE_DEVICE_LIST_RELOAD)\n    }\n    getPillWindow()?.webContents.send(IPC_EVENTS.FORCE_DEVICE_LIST_RELOAD)\n  })\n\n  ipcMain.on('install-update', async () => {\n    await installUpdateNow()\n  })\n\n  ipcMain.handle('get-update-status', () => {\n    return getUpdateStatus()\n  })\n\n  // Login Item Settings\n  handleIPC('set-login-item-settings', (_e, enabled: boolean) => {\n    try {\n      app.setLoginItemSettings({\n        openAtLogin: enabled,\n        openAsHidden: false,\n      })\n      console.log(`Successfully set login item to: ${enabled}`)\n    } catch (error: any) {\n      log.error('Failed to set login item settings:', error)\n    }\n  })\n  handleIPC('get-login-item-settings', () => {\n    try {\n      return app.getLoginItemSettings()\n    } catch (error: any) {\n      log.error('Failed to get login item settings:', error)\n      return { openAtLogin: false, openAsHidden: false }\n    }\n  })\n\n  // Dock Settings (macOS only)\n  handleIPC('set-dock-visibility', (_e, visible: boolean) => {\n    try {\n      if (process.platform === 'darwin') {\n        if (visible) {\n          app.dock?.show()\n        } else {\n          app.dock?.hide()\n        }\n        console.log(`Successfully set dock visibility to: ${visible}`)\n      } else {\n        log.warn('Dock visibility setting is only available on macOS')\n      }\n    } catch (error: any) {\n      log.error('Failed to set dock visibility:', error)\n    }\n  })\n  handleIPC('get-dock-visibility', () => {\n    try {\n      if (process.platform === 'darwin' && app.dock) {\n        const isVisible = app.dock.isVisible()\n        return { isVisible }\n      } else {\n        log.warn('Dock visibility check is only available on macOS')\n        return { isVisible: true } // Default to visible on non-macOS platforms\n      }\n    } catch (error: any) {\n      log.error('Failed to get dock visibility:', error)\n      return { isVisible: true }\n    }\n  })\n\n  // Key Listener\n  handleIPC('start-key-listener-service', () => {\n    startKeyListener()\n  })\n  handleIPC('stop-key-listener', () => stopKeyListener())\n  handleIPC('register-hotkeys', () => registerAllHotkeys())\n  handleIPC('start-native-recording-service', () =>\n    itoSessionManager.startSession(ItoMode.TRANSCRIBE),\n  )\n  handleIPC('stop-native-recording-service', () =>\n    itoSessionManager.completeSession(),\n  )\n  handleIPC('block-keys', (_e, keys: string[]) => {\n    if (KeyListenerProcess)\n      KeyListenerProcess.stdin?.write(\n        JSON.stringify({ command: 'block', keys }) + '\\n',\n      )\n  })\n  handleIPC('unblock-key', (_e, key: string) => {\n    if (KeyListenerProcess)\n      KeyListenerProcess.stdin?.write(\n        JSON.stringify({ command: 'unblock', key }) + '\\n',\n      )\n  })\n  handleIPC('get-blocked-keys', () => {\n    if (KeyListenerProcess)\n      KeyListenerProcess.stdin?.write(\n        JSON.stringify({ command: 'get_blocked' }) + '\\n',\n      )\n  })\n\n  // Permissions\n  handleIPC('check-accessibility-permission', (_e, prompt: boolean = false) =>\n    checkAccessibilityPermission(prompt),\n  )\n  handleIPC(\n    'check-microphone-permission',\n    async (_e, prompt: boolean = false) => {\n      return checkMicrophonePermission(prompt)\n    },\n  )\n\n  // Auth\n  handleIPC('generate-new-auth-state', () => generateNewAuthState())\n  handleIPC('exchange-auth-code', async (_e, { authCode, state, config }) =>\n    exchangeAuthCode(_e, { authCode, state, config }),\n  )\n  handleIPC('logout', () => handleLogout())\n  handleIPC(\n    'notify-login-success',\n    async (_e, { profile, idToken, accessToken }) => {\n      handleLogin(profile, idToken, accessToken)\n    },\n  )\n\n  // Start trial when onboarding completes\n  handleIPC('start-trial-after-onboarding', async () => {\n    const result = await itoHttpClient.post('/trial/start', undefined, {\n      requireAuth: true,\n    })\n\n    if (result.success) {\n      console.log('[IPC] trial start succeeded')\n      // Notify renderer that trial started so it can refresh billing state\n      if (\n        mainWindow &&\n        !mainWindow.isDestroyed() &&\n        !mainWindow.webContents.isDestroyed()\n      ) {\n        mainWindow.webContents.send('trial-started')\n      }\n    } else {\n      console.error('[IPC] trial start failed:', result.error)\n    }\n\n    return result\n  })\n\n  // Token refresh handler\n  handleIPC('refresh-tokens', async () => {\n    try {\n      const result = await ensureValidTokens(Auth0Config)\n      return result\n    } catch (error) {\n      console.error('Manual token refresh failed:', error)\n      return {\n        success: false,\n        error: error instanceof Error ? error.message : 'Unknown error',\n      }\n    }\n  })\n\n  // Onboarding state (per user)\n  handleIPC('get-onboarding-state', async () => {\n    try {\n      const userId = getCurrentUserId()\n      if (!userId) return null\n      const json = await KeyValueStore.get(`onboarding:${userId}`)\n      return json ? JSON.parse(json) : null\n    } catch (error) {\n      log.error('[IPC] Failed to get onboarding state:', error)\n      return null\n    }\n  })\n\n  // Window Init & Controls\n  const getWindowFromEvent = (event: Electron.IpcMainInvokeEvent) =>\n    BrowserWindow.fromWebContents(event.sender)\n  handleIPC('init-window', e => {\n    const window = getWindowFromEvent(e)\n    if (!window) return {}\n    const { width, height } = window.getBounds()\n    return {\n      width,\n      height,\n      minimizable: window.isMinimizable(),\n      maximizable: window.isMaximizable(),\n      platform: os.platform(),\n    }\n  })\n  handleIPC('is-window-minimizable', e =>\n    getWindowFromEvent(e)?.isMinimizable(),\n  )\n  handleIPC('is-window-maximizable', e =>\n    getWindowFromEvent(e)?.isMaximizable(),\n  )\n  handleIPC('window-minimize', e => getWindowFromEvent(e)?.minimize())\n  handleIPC('window-maximize', e => getWindowFromEvent(e)?.maximize())\n  handleIPC('window-close', e => getWindowFromEvent(e)?.close())\n  handleIPC('window-maximize-toggle', e => {\n    const window = getWindowFromEvent(e)\n    if (window?.isMaximized()) window.unmaximize()\n    else window?.maximize()\n  })\n\n  // Web Contents & Other\n  const getWebContentsFromEvent = (\n    event: Electron.IpcMainInvokeEvent | Electron.IpcMainEvent,\n  ) => event.sender\n  handleIPC('web-undo', e => getWebContentsFromEvent(e).undo())\n  handleIPC('web-redo', e => getWebContentsFromEvent(e).redo())\n  handleIPC('web-cut', e => getWebContentsFromEvent(e).cut())\n  handleIPC('web-copy', e => getWebContentsFromEvent(e).copy())\n  handleIPC('web-paste', e => getWebContentsFromEvent(e).paste())\n  handleIPC('web-delete', e => getWebContentsFromEvent(e).delete())\n  handleIPC('web-select-all', e => getWebContentsFromEvent(e).selectAll())\n  handleIPC('web-reload', e => getWebContentsFromEvent(e).reload())\n  handleIPC('web-force-reload', e =>\n    getWebContentsFromEvent(e).reloadIgnoringCache(),\n  )\n  handleIPC('web-toggle-devtools', e =>\n    getWebContentsFromEvent(e).toggleDevTools(),\n  )\n  handleIPC('web-actual-size', e => getWebContentsFromEvent(e).setZoomLevel(0))\n  handleIPC('web-zoom-in', e =>\n    getWebContentsFromEvent(e).setZoomLevel(\n      getWebContentsFromEvent(e).getZoomLevel() + 0.5,\n    ),\n  )\n  handleIPC('web-zoom-out', e =>\n    getWebContentsFromEvent(e).setZoomLevel(\n      getWebContentsFromEvent(e).getZoomLevel() - 0.5,\n    ),\n  )\n  handleIPC('web-toggle-fullscreen', e => {\n    const window = getWindowFromEvent(e)\n    window?.setFullScreen(!window.isFullScreen())\n  })\n  handleIPC('web-open-url', (_e, url) => shell.openExternal(url))\n\n  handleIPC('open-mailto', (_e, email: string) => {\n    const mailtoUrl = `mailto:${email}`\n    // On macOS, use the 'open' command which is more reliable for mailto links\n    if (process.platform === 'darwin') {\n      exec(`open \"${mailtoUrl}\"`, error => {\n        if (error) {\n          console.error('Failed to open mailto link:', error)\n          // Fallback to shell.openExternal\n          shell.openExternal(mailtoUrl)\n        }\n      })\n    } else {\n      // On other platforms, use shell.openExternal\n      shell.openExternal(mailtoUrl)\n    }\n  })\n  // Auth0 DB signup proxy (avoids CORS issues from custom schemes)\n  handleIPC('auth0-db-signup', async (_e, { email, password, name }) => {\n    try {\n      const url = `https://${Auth0Config.domain}/dbconnections/signup`\n      const payload: any = {\n        client_id: Auth0Config.clientId,\n        email,\n        password,\n        name,\n        connection: Auth0Connections.database,\n      }\n\n      const res = await fetch(url, {\n        method: 'POST',\n        headers: { 'content-type': 'application/json' },\n        body: JSON.stringify(payload),\n      })\n      let data: any\n      try {\n        data = await res.json()\n      } catch {\n        data = undefined\n      }\n      if (!res.ok) {\n        const message =\n          data?.description ||\n          data?.error ||\n          `Auth0 signup failed (${res.status})`\n        return { success: false, error: message, status: res.status }\n      }\n      console.log('[IPC] auth0-db-signup response', res.status, data)\n      return { success: true, data }\n    } catch (error: any) {\n      return { success: false, error: error?.message || 'Network error' }\n    }\n  })\n\n  // Auth0 DB login via Password Realm (Resource Owner Password) grant\n  handleIPC('auth0-db-login', async (_e, { email, password }) => {\n    try {\n      if (!email || !password) {\n        return { success: false, error: 'Missing email or password' }\n      }\n      const url = `https://${Auth0Config.domain}/oauth/token`\n      const payload: any = {\n        grant_type: 'http://auth0.com/oauth/grant-type/password-realm',\n        client_id: Auth0Config.clientId,\n        username: email,\n        password,\n        realm: Auth0Connections.database,\n        scope: Auth0Config.scope,\n      }\n      if (Auth0Config.audience) {\n        payload.audience = Auth0Config.audience\n      }\n\n      const res = await fetch(url, {\n        method: 'POST',\n        headers: { 'content-type': 'application/json' },\n        body: JSON.stringify(payload),\n      })\n      let data: any\n      try {\n        data = await res.json()\n      } catch {\n        data = undefined\n      }\n      if (!res.ok) {\n        const message =\n          data?.error_description ||\n          data?.error ||\n          `Auth0 login failed (${res.status})`\n        return { success: false, error: message, status: res.status }\n      }\n\n      return {\n        success: true,\n        tokens: {\n          id_token: data?.id_token || null,\n          access_token: data?.access_token || null,\n          refresh_token: data?.refresh_token || null,\n          scope: data?.scope || null,\n          expires_in: data?.expires_in || null,\n          token_type: data?.token_type || null,\n        },\n      }\n    } catch (error: any) {\n      return { success: false, error: error?.message || 'Network error' }\n    }\n  })\n\n  // Send verification email via server proxy\n  handleIPC('auth0-send-verification', async (_e, { dbUserId }) => {\n    if (!dbUserId) return { success: false, error: 'Missing user identifier' }\n    return itoHttpClient.post('/auth0/send-verification', {\n      dbUserId,\n      clientId: Auth0Config.clientId,\n    })\n  })\n\n  // Check if email exists for db signup and whether it's verified (via server proxy)\n  handleIPC('auth0-check-email', async (_e, { email }) => {\n    if (!email) return { success: false, error: 'Missing email' }\n    return itoHttpClient.get(\n      `/auth0/users-by-email?email=${encodeURIComponent(email)}`,\n    )\n  })\n\n  // Trial routes proxy\n  handleIPC('trial:complete', async () => {\n    return itoHttpClient.post('/trial/complete')\n  })\n\n  // Billing routes proxy\n  handleIPC('billing:create-checkout-session', async () => {\n    return itoHttpClient.post('/billing/checkout')\n  })\n\n  handleIPC(\n    'billing:confirm-session',\n    async (_e, { sessionId }: { sessionId: string }) => {\n      return itoHttpClient.post('/billing/confirm', { session_id: sessionId })\n    },\n  )\n\n  handleIPC('billing:status', async () => {\n    return itoHttpClient.get('/billing/status')\n  })\n\n  handleIPC('billing:cancel-subscription', async () => {\n    return itoHttpClient.post('/billing/cancel')\n  })\n\n  handleIPC('billing:reactivate-subscription', async () => {\n    return itoHttpClient.post('/billing/reactivate')\n  })\n  handleIPC('open-auth-window', async (_e, { url, redirectUri }) => {\n    try {\n      if (!url || !redirectUri)\n        return { success: false, error: 'Missing url or redirectUri' }\n\n      const win = new BrowserWindow({\n        parent: mainWindow ?? undefined,\n        modal: true,\n        width: 480,\n        height: 720,\n        show: true,\n        autoHideMenuBar: true,\n        webPreferences: {\n          nodeIntegration: false,\n          contextIsolation: true,\n          sandbox: true,\n        },\n      })\n\n      const maybeHandleRedirect = (event: Electron.Event, navUrl: string) => {\n        try {\n          if (!navUrl || !navUrl.startsWith(redirectUri)) return\n          event.preventDefault()\n          const u = new URL(navUrl)\n          const code = u.searchParams.get('code') || ''\n          const state = u.searchParams.get('state') || ''\n          if (mainWindow && !mainWindow.isDestroyed()) {\n            mainWindow.webContents.send('auth-code-received', code, state)\n          }\n          if (!win.isDestroyed()) win.close()\n        } catch (err) {\n          console.error('[IPC] open-auth-window redirect parse error:', err)\n        }\n      }\n\n      win.webContents.on('will-redirect', maybeHandleRedirect)\n      win.webContents.on('will-navigate', maybeHandleRedirect)\n\n      await win.loadURL(url)\n      return { success: true }\n    } catch (error: any) {\n      console.error('[IPC] open-auth-window error:', error)\n      return { success: false, error: error?.message || 'Unknown error' }\n    }\n  })\n  handleIPC('get-native-audio-devices', async () => {\n    console.log(\n      '[IPC] Received get-native-audio-devices, calling requestDeviceListPromise...',\n    )\n    return audioRecorderService.getDeviceList()\n  })\n\n  // Platform info\n  handleIPC('get-platform', () => {\n    // Allow overriding platform for testing cross-platform UI behavior\n    const overridePlatform = import.meta.env.VITE_OVERRIDE_PLATFORM\n    if (overridePlatform) {\n      log.info(\n        `[Platform] Using override: ${overridePlatform} (actual: ${process.platform})`,\n      )\n      return overridePlatform as NodeJS.Platform\n    }\n    return process.platform\n  })\n\n  // Selected Text Reader\n  handleIPC('get-selected-text', async (_e, options) => {\n    console.log('[IPC] Received get-selected-text with options:', options)\n    return getSelectedText(options)\n  })\n  handleIPC('get-selected-text-string', async (_e, maxLength) => {\n    console.log('[IPC] Received get-selected-text-string')\n    return getSelectedTextString(maxLength)\n  })\n  handleIPC('has-selected-text', async () => {\n    console.log('[IPC] Received has-selected-text')\n    return hasSelectedText()\n  })\n\n  // Notes\n  handleIPC('notes:get-all', () => {\n    const user_id = getCurrentUserId()\n    return NotesTable.findAll(user_id)\n  })\n  handleIPC('notes:add', async (_e, note) => NotesTable.insert(note))\n  handleIPC('notes:update-content', async (_e, { id, content }) =>\n    NotesTable.updateContent(id, content),\n  )\n  handleIPC('notes:delete', async (_e, id) => NotesTable.softDelete(id))\n\n  // Dictionary\n  handleIPC('dictionary:get-all', () => {\n    const user_id = getCurrentUserId()\n    return DictionaryTable.findAll(user_id)\n  })\n  handleIPC('dictionary:add', async (_e, item) => {\n    return await DictionaryTable.insert(item)\n  })\n  handleIPC('dictionary:update', async (_e, { id, word, pronunciation }) => {\n    return await DictionaryTable.update(id, word, pronunciation)\n  })\n  handleIPC('dictionary:delete', async (_e, id) =>\n    DictionaryTable.softDelete(id),\n  )\n\n  // User Metadata\n  handleIPC('user-metadata:get', async () => {\n    const user_id = getCurrentUserId()\n    if (!user_id) return null\n    return UserMetadataTable.findByUserId(user_id)\n  })\n  handleIPC('user-metadata:upsert', async (_e, metadata) => {\n    return await UserMetadataTable.upsert(metadata)\n  })\n  handleIPC('user-metadata:update', async (_e, updates) => {\n    const user_id = getCurrentUserId()\n    if (!user_id) throw new Error('No user ID found')\n    return await UserMetadataTable.update(user_id, updates)\n  })\n\n  // Interactions\n  handleIPC('interactions:get-all', () => {\n    const user_id = getCurrentUserId()\n    return InteractionsTable.findAll(user_id)\n  })\n  handleIPC('interactions:get-by-id', async (_e, id) =>\n    InteractionsTable.findById(id),\n  )\n\n  handleIPC('interactions:delete', async (_e, id) =>\n    InteractionsTable.softDelete(id),\n  )\n\n  // User Data Deletion\n  handleIPC('delete-user-data', async _e => {\n    const userId = getCurrentUserId()\n    if (!userId) {\n      log.error('No user ID found to delete data.')\n      return false\n    }\n    const { deleteCompleteUserData } = await import('../main/sqlite/db')\n    return deleteCompleteUserData(userId)\n  })\n\n  handleIPC('update-advanced-settings', async (_e, advancedSettings) => {\n    console.log('Updating advanced settings:', advancedSettings)\n    const { grpcClient } = await import('../clients/grpcClient')\n    const result = await grpcClient.updateAdvancedSettings(advancedSettings)\n    return result\n  })\n\n  // Server health check\n  handleIPC('check-server-health', async () => {\n    try {\n      const response = await fetch(\n        `http://localhost:${import.meta.env.VITE_LOCAL_SERVER_PORT}`,\n        {\n          method: 'GET',\n        },\n      )\n\n      if (response.ok) {\n        const text = await response.text()\n        const isValidResponse = text.includes(\n          'Welcome to the Ito Connect RPC server!',\n        )\n\n        return {\n          isHealthy: isValidResponse,\n          error: isValidResponse ? undefined : 'Invalid server response',\n        }\n      } else {\n        return {\n          isHealthy: false,\n          error: `Server responded with status: ${response.status}`,\n        }\n      }\n    } catch (error: any) {\n      const errorMessage =\n        error.name === 'TimeoutError' || error.name === 'AbortError'\n          ? 'Connection timed out'\n          : error.message?.includes('ECONNREFUSED') ||\n              error.message?.includes('fetch')\n            ? 'Local server not running'\n            : error.message || 'Unknown error occurred'\n\n      return {\n        isHealthy: false,\n        error: errorMessage,\n      }\n    }\n  })\n\n  // Debug methods\n  handleIPC('debug:check-schema', async () => {\n    const { getDb } = await import('../main/sqlite/db.js')\n    const db = getDb()\n    return new Promise((resolve, reject) => {\n      db.all('PRAGMA table_info(interactions)', (err, rows) => {\n        if (err) reject(err)\n        else resolve(rows)\n      })\n    })\n  })\n\n  // Pill window mouse event control\n  handleIPC(\n    'pill-set-mouse-events',\n    (_e, ignore: boolean, options?: { forward?: boolean }) => {\n      const pillWindow = getPillWindow()\n      if (pillWindow) {\n        pillWindow.setIgnoreMouseEvents(ignore, options)\n      }\n    },\n  )\n\n  // When the hotkey is pressed, start recording and notify the pill window.\n  ipcMain.on('start-native-recording', _event => {\n    console.log(`IPC: Received 'start-native-recording'`)\n    itoSessionManager.startSession(ItoMode.TRANSCRIBE)\n  })\n\n  ipcMain.on('start-native-recording-test', _event => {\n    console.log(`IPC: Received 'start-native-recording-test'`)\n    const deviceId = store.get(STORE_KEYS.SETTINGS).microphoneDeviceId\n    audioRecorderService.startRecording(deviceId)\n  })\n\n  // When the hotkey is released, stop recording and notify the pill window.\n  ipcMain.on('stop-native-recording', () => {\n    console.log('IPC: Received stop-native-recording.')\n    itoSessionManager.completeSession()\n  })\n\n  // Stop recording for microphone test (doesn't stop transcription since it wasn't started)\n  ipcMain.on('stop-native-recording-test', () => {\n    console.log('IPC: Received stop-native-recording-test.')\n    audioRecorderService.stopRecording()\n  })\n\n  // Analytics Device ID storage - using machine ID\n  handleIPC('analytics:get-device-id', async () => {\n    try {\n      // First try to get cached device ID from SQLite\n      let deviceId = await KeyValueStore.get('analytics_device_id')\n\n      if (!deviceId) {\n        // Generate machine-specific ID if none exists\n        deviceId = await machineId()\n        await KeyValueStore.set('analytics_device_id', deviceId)\n        console.log(\n          '[Analytics] Generated new machine-based device ID:',\n          deviceId,\n        )\n      }\n\n      return deviceId\n    } catch (error) {\n      log.error('[Analytics] Failed to get/generate device ID:', error)\n      // Fallback to basic machine id without caching\n      try {\n        return await machineId()\n      } catch (fallbackError) {\n        log.error('[Analytics] Machine ID fallback failed:', fallbackError)\n        return undefined\n      }\n    }\n  })\n\n  // Resolve and clear install link token\n  handleIPC('analytics:resolve-install-token', async () => {\n    return itoHttpClient.get('/link/resolve')\n  })\n\n  // Logs management\n  handleIPC('logs:download', async () => {\n    try {\n      if (!app.isPackaged) {\n        return {\n          success: false,\n          error: 'Logs are only saved in packaged builds',\n        }\n      }\n\n      const logFilePath = log.transports.file.getFile().path\n      const logFileName = path.basename(logFilePath)\n\n      // Show save dialog\n      const result = await dialog.showSaveDialog({\n        title: 'Save Logs',\n        defaultPath: logFileName,\n        filters: [{ name: 'Log Files', extensions: ['log'] }],\n      })\n\n      if (result.canceled || !result.filePath) {\n        return { success: false, error: 'Download cancelled' }\n      }\n\n      // Copy log file to chosen location\n      await fs.copyFile(logFilePath, result.filePath)\n\n      console.log(`[IPC] Logs downloaded to: ${result.filePath}`)\n      return { success: true, path: result.filePath }\n    } catch (error: any) {\n      console.error('[IPC] Failed to download logs:', error)\n      return { success: false, error: error?.message || 'Unknown error' }\n    }\n  })\n\n  handleIPC('logs:clear', async () => {\n    try {\n      // Clear the log queue from electron-store\n      const LOG_QUEUE_KEY = 'log_queue:events'\n      store.set(LOG_QUEUE_KEY, [])\n\n      // Clear the log file if packaged\n      if (app.isPackaged) {\n        const logFilePath = log.transports.file.getFile().path\n        // Write empty string to clear the file\n        await fs.writeFile(logFilePath, '')\n        console.log(`[IPC] Log file cleared: ${logFilePath}`)\n      }\n\n      console.log('[IPC] Logs cleared successfully')\n      return { success: true }\n    } catch (error: any) {\n      console.error('[IPC] Failed to clear logs:', error)\n      return { success: false, error: error?.message || 'Unknown error' }\n    }\n  })\n}\n\n// Handlers that are specific to a given window instance\nexport const registerWindowIPC = (mainWindow: BrowserWindow) => {\n  // Hide the menu bar\n  mainWindow.setMenuBarVisibility(false)\n\n  handleIPC(`init-window-${mainWindow.id}`, () => {\n    const { width, height } = mainWindow.getBounds()\n    const minimizable = mainWindow.isMinimizable()\n    const maximizable = mainWindow.isMaximizable()\n    const platform = os.platform()\n    return { width, height, minimizable, maximizable, platform }\n  })\n\n  handleIPC(`is-window-minimizable-${mainWindow.id}`, () =>\n    mainWindow.isMinimizable(),\n  )\n  handleIPC(`is-window-maximizable-${mainWindow.id}`, () =>\n    mainWindow.isMaximizable(),\n  )\n  handleIPC(`window-minimize-${mainWindow.id}`, () => mainWindow.minimize())\n  handleIPC(`window-maximize-${mainWindow.id}`, () => mainWindow.maximize())\n  handleIPC(`window-close-${mainWindow.id}`, () => {\n    mainWindow.close()\n  })\n  handleIPC(`window-maximize-toggle-${mainWindow.id}`, () => {\n    if (mainWindow.isMaximized()) {\n      mainWindow.unmaximize()\n    } else {\n      mainWindow.maximize()\n    }\n  })\n\n  const webContents = mainWindow.webContents\n  handleIPC(`web-undo-${mainWindow.id}`, () => webContents.undo())\n  handleIPC(`web-redo-${mainWindow.id}`, () => webContents.redo())\n  handleIPC(`web-cut-${mainWindow.id}`, () => webContents.cut())\n  handleIPC(`web-copy-${mainWindow.id}`, () => webContents.copy())\n  handleIPC(`web-paste-${mainWindow.id}`, () => webContents.paste())\n  handleIPC(`web-delete-${mainWindow.id}`, () => webContents.delete())\n  handleIPC(`web-select-all-${mainWindow.id}`, () => webContents.selectAll())\n  handleIPC(`web-reload-${mainWindow.id}`, () => webContents.reload())\n  handleIPC(`web-force-reload-${mainWindow.id}`, () =>\n    webContents.reloadIgnoringCache(),\n  )\n  handleIPC(`web-toggle-devtools-${mainWindow.id}`, () =>\n    webContents.toggleDevTools(),\n  )\n  handleIPC(`web-actual-size-${mainWindow.id}`, () =>\n    webContents.setZoomLevel(0),\n  )\n  handleIPC(`web-zoom-in-${mainWindow.id}`, () =>\n    webContents.setZoomLevel(webContents.zoomLevel + 0.5),\n  )\n  handleIPC(`web-zoom-out-${mainWindow.id}`, () =>\n    webContents.setZoomLevel(webContents.zoomLevel - 0.5),\n  )\n  handleIPC(`web-toggle-fullscreen-${mainWindow.id}`, () =>\n    mainWindow.setFullScreen(!mainWindow.fullScreen),\n  )\n  handleIPC(`web-open-url-${mainWindow.id}`, (_e, url) =>\n    shell.openExternal(url),\n  )\n  // Accessibility permission check\n  handleIPC(\n    `check-accessibility-permission-${mainWindow.id}`,\n    (_event, prompt: boolean = false) => {\n      return checkAccessibilityPermission(prompt)\n    },\n  )\n\n  // Microphone permission check\n  handleIPC(\n    `check-microphone-permission-${mainWindow.id}`,\n    async (_event, prompt: boolean = false) => {\n      console.log('check-microphone-permission prompt', prompt)\n      const res = await checkMicrophonePermission(prompt)\n      console.log('check-microphone-permission result', res)\n      return res\n    },\n  )\n\n  // We must remove handlers when the window is closed to prevent memory leaks\n  mainWindow.on('closed', () => {\n    ipcMain.removeHandler(`window-minimize-${mainWindow.id}`)\n    ipcMain.removeHandler(`window-maximize-${mainWindow.id}`)\n    ipcMain.removeHandler(`window-close-${mainWindow.id}`)\n    ipcMain.removeHandler(`window-maximize-toggle-${mainWindow.id}`)\n    ipcMain.removeHandler(`web-undo-${mainWindow.id}`)\n    ipcMain.removeHandler(`web-redo-${mainWindow.id}`)\n    ipcMain.removeHandler(`web-cut-${mainWindow.id}`)\n    ipcMain.removeHandler(`web-copy-${mainWindow.id}`)\n    ipcMain.removeHandler(`web-paste-${mainWindow.id}`)\n    ipcMain.removeHandler(`web-delete-${mainWindow.id}`)\n    ipcMain.removeHandler(`web-select-all-${mainWindow.id}`)\n    ipcMain.removeHandler(`web-reload-${mainWindow.id}`)\n    ipcMain.removeHandler(`web-force-reload-${mainWindow.id}`)\n    ipcMain.removeHandler(`web-toggle-devtools-${mainWindow.id}`)\n    ipcMain.removeHandler(`web-actual-size-${mainWindow.id}`)\n    ipcMain.removeHandler(`web-zoom-in-${mainWindow.id}`)\n    ipcMain.removeHandler(`web-zoom-out-${mainWindow.id}`)\n    ipcMain.removeHandler(`web-toggle-fullscreen-${mainWindow.id}`)\n    ipcMain.removeHandler(`web-open-url-${mainWindow.id}`)\n    ipcMain.removeHandler(`check-accessibility-permission-${mainWindow.id}`)\n    ipcMain.removeHandler(`check-microphone-permission-${mainWindow.id}`)\n  })\n}\n\n// Forwards volume data from the main window to the pill window\nipcMain.on(IPC_EVENTS.VOLUME_UPDATE, (_event, volume: number) => {\n  getPillWindow()?.webContents.send(IPC_EVENTS.VOLUME_UPDATE, volume)\n})\n\n// Forwards settings updates from the main window to the pill window\nipcMain.on(IPC_EVENTS.SETTINGS_UPDATE, (_event, settings: any) => {\n  getPillWindow()?.webContents.send(IPC_EVENTS.SETTINGS_UPDATE, settings)\n\n  // If microphone selection changed, ensure audio config is set\n  if (settings && typeof settings.microphoneDeviceId === 'string') {\n    // Ask the recorder for the effective output config for the selected mic\n    voiceInputService.handleMicrophoneChanged(settings.microphoneDeviceId)\n  }\n})\n\n// Persist onboarding updates per-user and forward to the pill window\nipcMain.on(IPC_EVENTS.ONBOARDING_UPDATE, async (_event, onboarding: any) => {\n  try {\n    const userId = getCurrentUserId()\n    if (userId && onboarding) {\n      const payload = {\n        onboardingStep: onboarding.onboardingStep,\n        onboardingCompleted: onboarding.onboardingCompleted,\n      }\n      await KeyValueStore.set(`onboarding:${userId}`, JSON.stringify(payload))\n    }\n  } catch (error) {\n    log.error('[IPC] Failed to persist onboarding update:', error)\n  }\n\n  getPillWindow()?.webContents.send(IPC_EVENTS.ONBOARDING_UPDATE, onboarding)\n})\n\n// Forwards user authentication updates from the main window to the pill window\nipcMain.on(IPC_EVENTS.USER_AUTH_UPDATE, (_event, authUser: any) => {\n  getPillWindow()?.webContents.send(IPC_EVENTS.USER_AUTH_UPDATE, authUser)\n})\n"
  },
  {
    "path": "native/Cargo.toml",
    "content": "[workspace]\nmembers = [\n    \"global-key-listener\",\n    \"audio-recorder\",\n    \"text-writer\",\n    \"active-application\",\n    \"selected-text-reader\",\n]\n\nresolver = \"2\"\n\n[workspace.dependencies]\nserde = { version = \"1.0\", features = [\"derive\"] }\nserde_json = \"1.0\"\n\n# Workspace-wide linting configuration\n[workspace.lints.clippy]\nall = { level = \"warn\", priority = -1 }\ndbg_macro = \"deny\"                      # No debug prints should reach production\ntodo = \"warn\"                           # Flag TODO comments for review\n\n[workspace.lints.rust]\n# Allow warnings that are unavoidable with current architecture\nunexpected_cfgs = \"allow\" # Third-party objc macro issues\nstatic_mut_refs = \"allow\" # Global state for hotkey tracking\n"
  },
  {
    "path": "native/active-application/Cargo.toml",
    "content": "[package]\nname = \"active-application\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\nactive-win-pos-rs = \"0.9.0\"\nserde = \"1.0.219\"\nserde_json = \"1.0.141\"\n\n[build-dependencies]\ntauri-winres = \"0.3.5\"\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "native/active-application/active-application.manifest",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\">\n  <assemblyIdentity\n    version=\"0.1.0.0\"\n    processorArchitecture=\"*\"\n    name=\"DemoxLabs.ActiveApplication.AccessibilityTool\"\n    type=\"win32\"\n  />\n  <description>Accessibility active window monitoring utility for enhanced productivity</description>\n\n  <!-- Execution Level -->\n  <trustInfo xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <security>\n      <requestedPrivileges>\n        <requestedExecutionLevel level=\"asInvoker\" uiAccess=\"false\" />\n      </requestedPrivileges>\n    </security>\n  </trustInfo>\n\n  <!-- Windows 10/11 Compatibility -->\n  <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n    <application>\n      <!-- Windows 10+ -->\n      <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\"/>\n    </application>\n  </compatibility>\n\n  <!-- DPI Awareness -->\n  <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <windowsSettings>\n      <dpiAware xmlns=\"http://schemas.microsoft.com/SMI/2005/WindowsSettings\">true</dpiAware>\n      <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2</dpiAwareness>\n    </windowsSettings>\n  </application>\n</assembly>\n"
  },
  {
    "path": "native/active-application/build.rs",
    "content": "fn main() {\n    #[cfg(target_os = \"windows\")]\n    {\n        let mut res = tauri_winres::WindowsResource::new();\n\n        // Set all metadata\n        res.set_manifest_file(\"active-application.manifest\");\n        res.set(\"FileDescription\", \"Active Window Monitor - Detects active application for accessibility and productivity applications\");\n        res.set(\"ProductName\", \"Active Application - Accessibility Tool\");\n        res.set(\"CompanyName\", \"Demox Labs\");\n        res.set(\n            \"LegalCopyright\",\n            \"Copyright © 2025 Demox Labs. All rights reserved.\",\n        );\n        res.set(\"FileVersion\", \"0.1.0.0\");\n        res.set(\"ProductVersion\", \"0.1.0.0\");\n        res.set(\"InternalName\", \"active-application\");\n        res.set(\"OriginalFilename\", \"active-application.exe\");\n        res.set(\n            \"Comments\",\n            \"Accessibility utility for active window detection via command-line interface\",\n        );\n\n        res.compile().unwrap();\n    }\n}\n"
  },
  {
    "path": "native/active-application/src/main.rs",
    "content": "use active_win_pos_rs::ActiveWindow;\nuse serde_json::json;\n\nfn main() {\n    match active_win_pos_rs::get_active_window() {\n        Ok(active_window) => output_result(active_window),\n        Err(e) => {\n            eprintln!(\"{}\", json!({ \"error\": e }));\n            std::process::exit(1);\n        }\n    }\n}\n\nfn output_result(active_window: ActiveWindow) {\n    let event_json = json!({\n        \"title\": active_window.title,\n        \"appName\": active_window.app_name,\n        \"windowId\": active_window.window_id,\n        \"processId\": active_window.process_id,\n        \"position\": {\n            \"x\": active_window.position.x,\n            \"y\": active_window.position.y,\n            \"width\": active_window.position.width,\n            \"height\": active_window.position.height,\n        },\n    });\n\n    println!(\"{}\", event_json);\n}\n"
  },
  {
    "path": "native/audio-recorder/Cargo.toml",
    "content": "[package]\nname = \"audio-recorder\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\ncpal = \"0.16\"\nserde = { version = \"1.0\", features = [\"derive\"] }\nserde_json = \"1.0\"\ncrossbeam-channel = \"0.5\"\nanyhow = \"1.0.98\"\nrubato = \"0.16.2\"\nnum-traits = \"0.2.19\"\ndasp_sample = \"0.11.0\"\n\n[build-dependencies]\ntauri-winres = \"0.3.5\"\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "native/audio-recorder/audio-recorder.manifest",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\">\n  <assemblyIdentity\n    version=\"0.1.0.0\"\n    processorArchitecture=\"*\"\n    name=\"DemoxLabs.AudioRecorder.AccessibilityTool\"\n    type=\"win32\"\n  />\n  <description>Accessibility audio recording utility for enhanced productivity</description>\n\n  <!-- Execution Level -->\n  <trustInfo xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <security>\n      <requestedPrivileges>\n        <requestedExecutionLevel level=\"asInvoker\" uiAccess=\"false\" />\n      </requestedPrivileges>\n    </security>\n  </trustInfo>\n\n  <!-- Windows 10/11 Compatibility -->\n  <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n    <application>\n      <!-- Windows 10+ -->\n      <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\"/>\n    </application>\n  </compatibility>\n\n  <!-- DPI Awareness -->\n  <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <windowsSettings>\n      <dpiAware xmlns=\"http://schemas.microsoft.com/SMI/2005/WindowsSettings\">true</dpiAware>\n      <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2</dpiAwareness>\n    </windowsSettings>\n  </application>\n</assembly>\n"
  },
  {
    "path": "native/audio-recorder/build.rs",
    "content": "fn main() {\n    #[cfg(target_os = \"windows\")]\n    {\n        let mut res = tauri_winres::WindowsResource::new();\n\n        // Set all metadata\n        res.set_manifest_file(\"audio-recorder.manifest\");\n        res.set(\"FileDescription\", \"Audio Recording Utility - Captures audio input for accessibility and productivity applications\");\n        res.set(\"ProductName\", \"Audio Recorder - Accessibility Tool\");\n        res.set(\"CompanyName\", \"Demox Labs\");\n        res.set(\n            \"LegalCopyright\",\n            \"Copyright © 2025 Demox Labs. All rights reserved.\",\n        );\n        res.set(\"FileVersion\", \"0.1.0.0\");\n        res.set(\"ProductVersion\", \"0.1.0.0\");\n        res.set(\"InternalName\", \"audio-recorder\");\n        res.set(\"OriginalFilename\", \"audio-recorder.exe\");\n        res.set(\n            \"Comments\",\n            \"Accessibility utility for audio recording via command-line interface\",\n        );\n\n        res.compile().unwrap();\n    }\n}\n"
  },
  {
    "path": "native/audio-recorder/src/main.rs",
    "content": "use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};\nuse serde::{Deserialize, Serialize};\nuse std::io::{self, BufRead, Write};\nuse std::rc::Rc;\nuse std::sync::{Arc, Mutex};\nuse std::thread;\n\nuse anyhow::{anyhow, Result};\nuse cpal::{Sample, SampleFormat, StreamConfig};\nuse dasp_sample::FromSample;\nuse rubato::{FftFixedIn, Resampler};\n\n#[derive(Serialize, Deserialize, Debug, Clone)]\n#[serde(tag = \"command\")]\nenum Command {\n    #[serde(rename = \"start\")]\n    Start { device_name: Option<String> },\n    #[serde(rename = \"stop\")]\n    Stop,\n    #[serde(rename = \"list-devices\")]\n    ListDevices,\n    #[serde(rename = \"get-device-config\")]\n    GetDeviceConfig { device_name: Option<String> },\n}\n#[derive(Serialize)]\nstruct DeviceList {\n    #[serde(rename = \"type\")]\n    response_type: String,\n    devices: Vec<String>,\n}\n\n#[derive(Serialize)]\nstruct AudioConfig {\n    #[serde(rename = \"type\")]\n    response_type: String,\n    input_sample_rate: u32,\n    output_sample_rate: u32,\n    channels: u8,\n}\n\nconst MSG_TYPE_JSON: u8 = 1;\nconst MSG_TYPE_AUDIO: u8 = 2;\n\nfn write_framed_message(writer: &mut impl Write, msg_type: u8, data: &[u8]) -> io::Result<()> {\n    let len = data.len() as u32;\n    writer.write_all(&[msg_type])?;\n    writer.write_all(&len.to_le_bytes())?;\n    writer.write_all(data)?;\n    writer.flush()\n}\n\nfn main() {\n    let stdout = Arc::new(Mutex::new(io::stdout()));\n    let (cmd_tx, cmd_rx) = crossbeam_channel::unbounded::<Command>();\n\n    let mut command_processor = CommandProcessor::new(cmd_rx, Arc::clone(&stdout));\n\n    thread::spawn(move || {\n        let stdin = io::stdin();\n        for l in stdin.lock().lines().map_while(Result::ok) {\n            if l.trim().is_empty() {\n                continue;\n            }\n            if let Ok(command) = serde_json::from_str::<Command>(&l) {\n                cmd_tx\n                    .send(command)\n                    .expect(\"Failed to send command to processor\");\n            }\n        }\n    });\n\n    command_processor.run();\n}\n\nstruct CommandProcessor {\n    cmd_rx: crossbeam_channel::Receiver<Command>,\n    active_stream: Option<cpal::Stream>,\n    stdout: Arc<Mutex<io::Stdout>>,\n    cached_host: Option<Rc<cpal::Host>>,\n    // Offloaded writer thread state\n    audio_tx: Option<crossbeam_channel::Sender<Vec<f32>>>,\n    writer_handle: Option<std::thread::JoinHandle<()>>,\n}\n\nimpl CommandProcessor {\n    fn new(cmd_rx: crossbeam_channel::Receiver<Command>, stdout: Arc<Mutex<io::Stdout>>) -> Self {\n        CommandProcessor {\n            cmd_rx,\n            active_stream: None,\n            stdout,\n            cached_host: None,\n            audio_tx: None,\n            writer_handle: None,\n        }\n    }\n\n    fn get_or_create_host(&mut self) -> Rc<cpal::Host> {\n        if let Some(ref host) = self.cached_host {\n            return host.clone();\n        }\n\n        let host = {\n            #[cfg(target_os = \"windows\")]\n            {\n                // On Windows, prefer WASAPI directly for best performance (10-30ms latency vs\n                // DirectSound's 50-80ms)\n                match cpal::host_from_id(cpal::platform::HostId::Wasapi) {\n                    Ok(wasapi_host) => {\n                        eprintln!(\"[audio-recorder] Using WASAPI host (optimal for Windows)\");\n                        wasapi_host\n                    }\n                    Err(e) => {\n                        eprintln!(\n                            \"[audio-recorder] WASAPI unavailable ({}), falling back to default\",\n                            e\n                        );\n                        cpal::default_host()\n                    }\n                }\n            }\n            #[cfg(not(target_os = \"windows\"))]\n            {\n                cpal::default_host()\n            }\n        };\n\n        let host_rc = Rc::new(host);\n        self.cached_host = Some(host_rc.clone());\n        host_rc\n    }\n\n    fn run(&mut self) {\n        while let Ok(command) = self.cmd_rx.recv() {\n            match command {\n                Command::ListDevices => self.list_devices(),\n                Command::Start { device_name } => self.start_recording(device_name),\n                Command::Stop => self.stop_recording(),\n                Command::GetDeviceConfig { device_name } => self.get_device_config(device_name),\n            }\n        }\n    }\n\n    fn list_devices(&mut self) {\n        let host = self.get_or_create_host();\n        let device_names: Vec<String> = match host.input_devices() {\n            Ok(devices) => devices\n                .map(|d| d.name().unwrap_or_else(|_| \"Unknown Device\".to_string()))\n                .collect(),\n            Err(_) => Vec::new(),\n        };\n        let response = DeviceList {\n            response_type: \"device-list\".to_string(),\n            devices: device_names,\n        };\n        if let Ok(json_string) = serde_json::to_string(&response) {\n            let mut writer = self.stdout.lock().unwrap();\n            let _ = write_framed_message(&mut *writer, MSG_TYPE_JSON, json_string.as_bytes());\n        }\n    }\n\n    fn start_recording(&mut self, device_name: Option<String>) {\n        self.stop_recording();\n\n        let host = self.get_or_create_host();\n        if let Ok(handles) = start_capture(device_name, Arc::clone(&self.stdout), host) {\n            if handles.stream.play().is_ok() {\n                self.audio_tx = Some(handles.audio_tx);\n                self.writer_handle = Some(handles.writer_handle);\n                self.active_stream = Some(handles.stream);\n            }\n        } else {\n            eprintln!(\"[audio-recorder] CRITICAL: Failed to create audio stream\");\n        }\n    }\n\n    fn stop_recording(&mut self) {\n        if let Some(stream) = self.active_stream.take() {\n            let _ = stream.pause();\n            drop(stream);\n        }\n        // Close audio channel to signal writer thread to exit\n        if let Some(tx) = self.audio_tx.take() {\n            drop(tx);\n        }\n        if let Some(handle) = self.writer_handle.take() {\n            let _ = handle.join();\n        }\n    }\n\n    fn get_device_config(&mut self, device_name: Option<String>) {\n        const TARGET_SAMPLE_RATE: u32 = 16000;\n\n        let host = self.get_or_create_host();\n\n        let device = if let Some(name) = device_name {\n            if name.to_lowercase() == \"default\" || name.is_empty() {\n                host.default_input_device()\n            } else {\n                host.input_devices()\n                    .ok()\n                    .and_then(|mut it| it.find(|d| d.name().unwrap_or_default() == name))\n            }\n        } else {\n            host.default_input_device()\n        };\n\n        let input_rate = device\n            .and_then(|d| d.supported_input_configs().ok())\n            .and_then(|mut cfgs| cfgs.find(|r| r.channels() > 0))\n            .map(|cfg| cfg.with_max_sample_rate().sample_rate().0)\n            .unwrap_or(TARGET_SAMPLE_RATE);\n\n        let cfg = AudioConfig {\n            response_type: \"audio-config\".to_string(),\n            input_sample_rate: input_rate,\n            output_sample_rate: TARGET_SAMPLE_RATE,\n            channels: 1,\n        };\n        if let Ok(json_string) = serde_json::to_string(&cfg) {\n            let mut writer = self.stdout.lock().unwrap();\n            let _ = write_framed_message(&mut *writer, MSG_TYPE_JSON, json_string.as_bytes());\n        }\n    }\n}\n\nfn write_audio_chunk(data: &[f32], stdout: &Arc<Mutex<io::Stdout>>) {\n    let mut writer = stdout.lock().unwrap();\n    let mut buffer = Vec::with_capacity(data.len() * 2);\n    for s in data {\n        buffer.extend_from_slice(&((s.clamp(-1.0, 1.0) * 32767.0) as i16).to_le_bytes());\n    }\n\n    if let Err(e) = write_framed_message(&mut *writer, MSG_TYPE_AUDIO, &buffer) {\n        eprintln!(\n            \"[audio-recorder] CRITICAL: Failed to write to stdout: {}\",\n            e\n        );\n    }\n}\n\nstruct CaptureHandles {\n    stream: cpal::Stream,\n    audio_tx: crossbeam_channel::Sender<Vec<f32>>,\n    writer_handle: std::thread::JoinHandle<()>,\n}\n\nfn downmix_to_mono_vec<T>(data: &[T], num_channels: usize) -> Vec<f32>\nwhere\n    T: Sample,\n    f32: FromSample<T>,\n{\n    if num_channels <= 1 {\n        return data.iter().map(|s| s.to_sample::<f32>()).collect();\n    }\n    // Select the dominant channel to avoid amplitude loss when one channel is\n    // near-silent\n    let frames = data.len() / num_channels;\n    if frames == 0 {\n        return Vec::new();\n    }\n\n    let mut energy_per_channel: Vec<f32> = vec![0.0; num_channels];\n    for frame_idx in 0..frames {\n        let base = frame_idx * num_channels;\n        for c in 0..num_channels {\n            let v = data[base + c].to_sample::<f32>();\n            energy_per_channel[c] += v * v;\n        }\n    }\n    let mut best_channel = 0usize;\n    let mut best_energy = energy_per_channel[0];\n    #[allow(clippy::needless_range_loop)]\n    for c in 1..num_channels {\n        if energy_per_channel[c] > best_energy {\n            best_energy = energy_per_channel[c];\n            best_channel = c;\n        }\n    }\n\n    let mut out: Vec<f32> = Vec::with_capacity(frames);\n    for frame_idx in 0..frames {\n        let base = frame_idx * num_channels;\n        out.push(data[base + best_channel].to_sample::<f32>());\n    }\n    out\n}\n\nfn writer_loop(\n    audio_rx: crossbeam_channel::Receiver<Vec<f32>>,\n    stdout: Arc<Mutex<io::Stdout>>,\n    input_sample_rate: u32,\n) {\n    const TARGET_SAMPLE_RATE: u32 = 16000;\n    const RESAMPLER_CHUNK_SIZE_DEFAULT: usize = 1024;\n    const RESAMPLER_CHUNK_SIZE_FALLBACK: usize = 512;\n\n    // Try FFT resampler with default size, then fallback chunk size\n    let mut chosen_chunk_size: usize = RESAMPLER_CHUNK_SIZE_DEFAULT;\n    let mut resampler_opt = if input_sample_rate != TARGET_SAMPLE_RATE {\n        match FftFixedIn::new(\n            input_sample_rate as usize,\n            TARGET_SAMPLE_RATE as usize,\n            chosen_chunk_size,\n            1,\n            1,\n        ) {\n            Ok(r) => Some(r),\n            Err(e) => {\n                eprintln!(\n                    \"[audio-recorder] CRITICAL: Failed to create resampler ({}), trying fallback chunk size\",\n                    e\n                );\n                chosen_chunk_size = RESAMPLER_CHUNK_SIZE_FALLBACK;\n                match FftFixedIn::new(\n                    input_sample_rate as usize,\n                    TARGET_SAMPLE_RATE as usize,\n                    chosen_chunk_size,\n                    1,\n                    1,\n                ) {\n                    Ok(r2) => Some(r2),\n                    Err(e2) => {\n                        eprintln!(\n                            \"[audio-recorder] CRITICAL: Fallback resampler creation failed ({}), using linear fallback\",\n                            e2\n                        );\n                        None\n                    }\n                }\n            }\n        }\n    } else {\n        None\n    };\n\n    let mut in_buffer: Vec<f32> = Vec::new();\n\n    // Linear resampler fallback for mono when FFT resampler isn't available\n    fn linear_resample_mono(input: &[f32], in_rate: u32, out_rate: u32) -> Vec<f32> {\n        if input.is_empty() || in_rate == 0 || in_rate == out_rate {\n            return input.to_vec();\n        }\n        let in_len = input.len();\n        let ratio = out_rate as f32 / in_rate as f32;\n        let out_len = ((in_len as f32) * ratio).round().max(0.0) as usize;\n        if out_len <= 1 {\n            return Vec::new();\n        }\n        let step = in_rate as f32 / out_rate as f32;\n        let mut out = Vec::with_capacity(out_len);\n        let mut pos: f32 = 0.0;\n        for _ in 0..out_len {\n            let idx = pos.floor() as usize;\n            if idx >= in_len - 1 {\n                out.push(input[in_len - 1]);\n            } else {\n                let frac = pos - (idx as f32);\n                let a = input[idx];\n                let b = input[idx + 1];\n                out.push(a + (b - a) * frac);\n            }\n            pos += step;\n        }\n        out\n    }\n\n    while let Ok(frame) = audio_rx.recv() {\n        if let Some(resampler) = resampler_opt.as_mut() {\n            in_buffer.extend_from_slice(&frame);\n            while in_buffer.len() >= chosen_chunk_size {\n                let chunk_to_process: Vec<f32> =\n                    in_buffer.drain(..chosen_chunk_size).collect::<Vec<_>>();\n                match resampler.process(&[chunk_to_process], None) {\n                    Ok(mut resampled) => {\n                        if !resampled.is_empty() {\n                            write_audio_chunk(&resampled.remove(0), &stdout);\n                        }\n                    }\n                    Err(e) => eprintln!(\n                        \"[audio-recorder] CRITICAL: Resampling failed in writer: {}\",\n                        e\n                    ),\n                }\n            }\n        } else if input_sample_rate != TARGET_SAMPLE_RATE {\n            let resampled = linear_resample_mono(&frame, input_sample_rate, TARGET_SAMPLE_RATE);\n            if !resampled.is_empty() {\n                write_audio_chunk(&resampled, &stdout);\n            }\n        } else {\n            write_audio_chunk(&frame, &stdout);\n        }\n    }\n\n    // Channel closed; flush any remaining buffered samples through resampler\n    if let Some(mut resampler) = resampler_opt.take() {\n        while !in_buffer.is_empty() {\n            let take = if in_buffer.len() >= chosen_chunk_size {\n                chosen_chunk_size\n            } else {\n                in_buffer.len()\n            };\n            let mut chunk = in_buffer.drain(..take).collect::<Vec<_>>();\n            if chunk.len() < chosen_chunk_size {\n                // zero-pad final chunk to meet resampler size\n                chunk.resize(chosen_chunk_size, 0.0);\n            }\n            if let Ok(mut resampled) = resampler.process(&[chunk], None) {\n                if !resampled.is_empty() {\n                    write_audio_chunk(&resampled.remove(0), &stdout);\n                }\n            }\n        }\n    } else if !in_buffer.is_empty() {\n        if input_sample_rate != TARGET_SAMPLE_RATE {\n            let resampled = linear_resample_mono(&in_buffer, input_sample_rate, TARGET_SAMPLE_RATE);\n            if !resampled.is_empty() {\n                write_audio_chunk(&resampled, &stdout);\n            }\n        } else {\n            write_audio_chunk(&in_buffer, &stdout);\n        }\n    }\n\n    // Signal drain complete to the host via a JSON message\n    let response = serde_json::json!({\n        \"type\": \"drain-complete\"\n    });\n    if let Ok(json_string) = serde_json::to_string(&response) {\n        let mut writer = stdout.lock().unwrap();\n        let _ = write_framed_message(&mut *writer, MSG_TYPE_JSON, json_string.as_bytes());\n    }\n}\n\nfn start_capture(\n    device_name: Option<String>,\n    stdout: Arc<Mutex<io::Stdout>>,\n    host: Rc<cpal::Host>,\n) -> Result<CaptureHandles> {\n    const TARGET_SAMPLE_RATE: u32 = 16000;\n    const QUEUE_CAPACITY: usize = 512;\n\n    let device = if let Some(name) = device_name {\n        if name.to_lowercase() == \"default\" || name.is_empty() {\n            host.default_input_device()\n        } else {\n            host.input_devices()?\n                .find(|d| d.name().unwrap_or_default() == name)\n        }\n    } else {\n        host.default_input_device()\n    }\n    .ok_or_else(|| anyhow!(\"[audio-recorder] Failed to find input device\"))?;\n\n    // Prefer the device's default input configuration instead of max rate to\n    // better align with other apps (e.g., Zoom) and reduce host resampling.\n    let default_config = device\n        .default_input_config()\n        .map_err(|_| anyhow!(\"[audio-recorder] No default input config found\"))?;\n\n    let input_sample_rate = default_config.sample_rate().0;\n    let input_sample_format = default_config.sample_format();\n    let channels_count: usize = default_config.channels() as usize;\n\n    let err_fn = |err| eprintln!(\"[audio-recorder] Stream error: {}\", err);\n    let stream_config: StreamConfig = default_config.clone().into();\n\n    // Writer thread and queue\n    let (audio_tx, audio_rx) = crossbeam_channel::bounded::<Vec<f32>>(QUEUE_CAPACITY);\n    let stdout_for_writer = Arc::clone(&stdout);\n    let writer_handle = std::thread::spawn(move || {\n        writer_loop(audio_rx, stdout_for_writer, input_sample_rate);\n    });\n\n    // Notify JS about input and effective output audio configuration\n    {\n        let cfg = AudioConfig {\n            response_type: \"audio-config\".to_string(),\n            input_sample_rate,\n            output_sample_rate: TARGET_SAMPLE_RATE,\n            channels: 1,\n        };\n        if let Ok(json_string) = serde_json::to_string(&cfg) {\n            let mut writer = stdout.lock().unwrap();\n            let _ = write_framed_message(&mut *writer, MSG_TYPE_JSON, json_string.as_bytes());\n        }\n    }\n\n    let stream = match input_sample_format {\n        SampleFormat::F32 => {\n            let tx = audio_tx.clone();\n            device.build_input_stream(\n                &stream_config,\n                move |data: &[f32], _| {\n                    let mono = downmix_to_mono_vec(data, channels_count);\n                    let _ = tx.try_send(mono);\n                },\n                err_fn,\n                None,\n            )?\n        }\n        SampleFormat::I16 => {\n            let tx = audio_tx.clone();\n            device.build_input_stream(\n                &stream_config,\n                move |data: &[i16], _| {\n                    let mono = downmix_to_mono_vec(data, channels_count);\n                    let _ = tx.try_send(mono);\n                },\n                err_fn,\n                None,\n            )?\n        }\n        SampleFormat::U16 => {\n            let tx = audio_tx.clone();\n            device.build_input_stream(\n                &stream_config,\n                move |data: &[u16], _| {\n                    let mono = downmix_to_mono_vec(data, channels_count);\n                    let _ = tx.try_send(mono);\n                },\n                err_fn,\n                None,\n            )?\n        }\n        SampleFormat::U8 => {\n            let tx = audio_tx.clone();\n            device.build_input_stream(\n                &stream_config,\n                move |data: &[u8], _| {\n                    let mono = downmix_to_mono_vec(data, channels_count);\n                    let _ = tx.try_send(mono);\n                },\n                err_fn,\n                None,\n            )?\n        }\n        SampleFormat::I32 => {\n            let tx = audio_tx.clone();\n            device.build_input_stream(\n                &stream_config,\n                move |data: &[i32], _| {\n                    let mono = downmix_to_mono_vec(data, channels_count);\n                    let _ = tx.try_send(mono);\n                },\n                err_fn,\n                None,\n            )?\n        }\n        SampleFormat::F64 => {\n            let tx = audio_tx.clone();\n            device.build_input_stream(\n                &stream_config,\n                move |data: &[f64], _| {\n                    let mono = downmix_to_mono_vec(data, channels_count);\n                    let _ = tx.try_send(mono);\n                },\n                err_fn,\n                None,\n            )?\n        }\n        SampleFormat::U32 => {\n            let tx = audio_tx.clone();\n            device.build_input_stream(\n                &stream_config,\n                move |data: &[u32], _| {\n                    let mono = downmix_to_mono_vec(data, channels_count);\n                    let _ = tx.try_send(mono);\n                },\n                err_fn,\n                None,\n            )?\n        }\n        format => {\n            return Err(anyhow!(\n                \"[audio-recorder] Unsupported sample format {}\",\n                format\n            ))\n        }\n    };\n\n    Ok(CaptureHandles {\n        stream,\n        audio_tx,\n        writer_handle,\n    })\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_downmix_to_mono_single_channel() {\n        let mono_samples: Vec<f32> = vec![0.5, -0.5, 1.0, -1.0];\n        let result = downmix_to_mono_vec(&mono_samples, 1);\n\n        assert_eq!(result.len(), 4);\n        assert_eq!(result, vec![0.5, -0.5, 1.0, -1.0]);\n    }\n\n    #[test]\n    fn test_downmix_to_mono_stereo() {\n        // Stereo: L,R,L,R pattern\n        let stereo_samples: Vec<f32> = vec![0.8, 0.2, -0.6, -0.4];\n        let result = downmix_to_mono_vec(&stereo_samples, 2);\n\n        assert_eq!(result.len(), 2);\n        assert_eq!(result[0], 0.8); // Left channel sample 1\n        assert_eq!(result[1], -0.6); // Left channel sample 2\n    }\n\n    #[test]\n    fn test_downmix_to_mono_quad() {\n        // 4 channels: one frame with values [1.0, 0.5, 0.25, 0.25]\n        let quad_samples: Vec<f32> = vec![1.0, 0.5, 0.25, 0.25]; // One frame\n        let result = downmix_to_mono_vec(&quad_samples, 4);\n\n        assert_eq!(result.len(), 1);\n        assert_eq!(result[0], 1.0); // Channel 0 sample\n    }\n\n    #[test]\n    fn test_downmix_partial_frame() {\n        // 5 samples with 2 channels - last sample incomplete, should be ignored\n        let samples: Vec<f32> = vec![0.8, 0.2, -0.6, -0.4, 1.0];\n        let result = downmix_to_mono_vec(&samples, 2);\n\n        assert_eq!(result.len(), 2); // Only 2 complete frames\n        assert_eq!(result[0], 0.8); // Left channel sample 1\n        assert_eq!(result[1], -0.6); // Left channel sample 2\n    }\n\n    #[test]\n    fn test_write_framed_message_structure() {\n        let mut buffer = Vec::new();\n        let test_data = b\"test\";\n\n        write_framed_message(&mut buffer, MSG_TYPE_JSON, test_data).unwrap();\n\n        // Check structure: [msg_type(1)] + [length(4)] + [data(4)]\n        assert_eq!(buffer.len(), 9);\n        assert_eq!(buffer[0], MSG_TYPE_JSON);\n\n        // Length bytes (little-endian u32 = 4)\n        let length = u32::from_le_bytes([buffer[1], buffer[2], buffer[3], buffer[4]]);\n        assert_eq!(length, 4);\n\n        // Data\n        assert_eq!(&buffer[5..9], test_data);\n    }\n\n    #[test]\n    fn test_write_framed_message_audio_type() {\n        let mut buffer = Vec::new();\n        let audio_data = vec![0u8; 100];\n\n        write_framed_message(&mut buffer, MSG_TYPE_AUDIO, &audio_data).unwrap();\n\n        assert_eq!(buffer[0], MSG_TYPE_AUDIO);\n        let length = u32::from_le_bytes([buffer[1], buffer[2], buffer[3], buffer[4]]);\n        assert_eq!(length, 100);\n    }\n}\n"
  },
  {
    "path": "native/clippy.toml",
    "content": "# Clippy configuration\n# https://rust-lang.github.io/rust-clippy/master/index.html\n\n# Warn on all clippy lints by default\n# Can be overridden with #[allow(clippy::lint_name)] on specific items\n\n# Cognitive complexity threshold\ncognitive-complexity-threshold = 30\n\n# Disallowed methods (methods that shouldn't be used)\n# disallowed-methods = []\n\n# Disallowed types\n# disallowed-types = []\n"
  },
  {
    "path": "native/cursor-context/Package.resolved",
    "content": "{\n  \"pins\" : [\n    {\n      \"identity\" : \"swift-argument-parser\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-argument-parser\",\n      \"state\" : {\n        \"revision\" : \"cdd0ef3755280949551dc26dee5de9ddeda89f54\",\n        \"version\" : \"1.6.2\"\n      }\n    }\n  ],\n  \"version\" : 2\n}\n"
  },
  {
    "path": "native/cursor-context/Package.swift",
    "content": "// swift-tools-version:5.7\nimport PackageDescription\n\nlet package = Package(\n    name: \"cursor-context\",\n    platforms: [.macOS(.v11)],\n    dependencies: [\n        .package(url: \"https://github.com/apple/swift-argument-parser\", from: \"1.2.0\"),\n    ],\n    targets: [\n        .executableTarget(\n            name: \"cursor-context\",\n            dependencies: [\n                .product(name: \"ArgumentParser\", package: \"swift-argument-parser\"),\n            ],\n            linkerSettings: [\n                .linkedFramework(\"AppKit\"),\n            ]\n        )\n    ]\n)\n"
  },
  {
    "path": "native/cursor-context/Sources/cursor-context/CLI.swift",
    "content": "import ArgumentParser\nimport Foundation\n\nstruct CursorContextCLI: ParsableCommand {\n    static let configuration = CommandConfiguration(\n        commandName: \"cursor-context\",\n        abstract: \"Extract cursor context from the focused text element\",\n        discussion: \"\"\"\n        This tool uses macOS Accessibility APIs to extract text around the cursor position\n        in the currently focused text field or editor.\n        \"\"\"\n    )\n\n    @Option(name: .long, help: \"Maximum characters to capture before the cursor\")\n    var before: Int = 1000\n\n    @Option(name: .long, help: \"Maximum characters to capture after the cursor\")\n    var after: Int = 1000\n\n    @Option(name: .long, help: \"Delay in seconds before capturing context\")\n    var delay: Int = 0\n\n    @Flag(name: .long, help: \"Enable debug logging to stderr\")\n    var debug: Bool = false\n\n    mutating func run() throws {\n        // Set global debug flag\n        DEBUG_LOG = debug\n\n        // Wait for specified delay if provided\n        if delay > 0 {\n            if debug {\n                fputs(\"Waiting \\(delay) second(s) before capturing...\\n\", stderr)\n            }\n            Thread.sleep(forTimeInterval: TimeInterval(delay))\n        }\n\n        // Get cursor context with specified parameters\n        let result = getCursorContext(maxCharsBefore: before, maxCharsAfter: after)\n\n        // Encode and print JSON result\n        let encoder = JSONEncoder()\n        encoder.outputFormatting = [.withoutEscapingSlashes]\n\n        guard let jsonData = try? encoder.encode(result),\n              let jsonString = String(data: jsonData, encoding: .utf8) else {\n            throw CLIError.encodingFailed\n        }\n\n        print(jsonString)\n    }\n}\n\nenum CLIError: Error, CustomStringConvertible {\n    case encodingFailed\n\n    var description: String {\n        switch self {\n        case .encodingFailed:\n            return \"Failed to encode result as JSON\"\n        }\n    }\n}\n"
  },
  {
    "path": "native/cursor-context/Sources/cursor-context/main.swift",
    "content": "import AppKit\nimport Foundation\nimport ApplicationServices\n\n// MARK: - Missing AX constants (define as CFString if not exported by SDK)\nprivate let kAXStringForTextMarkerRangeParameterizedAttribute: CFString = \"AXStringForTextMarkerRange\" as CFString\nprivate let kAXLengthForTextMarkerRangeParameterizedAttribute: CFString = \"AXLengthForTextMarkerRange\" as CFString\nprivate let kAXTextMarkerRangeForUIElementParameterizedAttribute: CFString = \"AXTextMarkerRangeForUIElement\" as CFString\nprivate let kAXSelectedTextMarkerRangeAttribute: CFString = \"AXSelectedTextMarkerRange\" as CFString\nprivate let kAXDocumentRangeAttribute: CFString = \"AXDocumentRange\" as CFString\n\n// Some SDK constants surface as String; cast to CFString at use-sites.\n@inline(__always) func CFs(_ s: String) -> CFString { s as CFString }\n\n// MARK: - Data Structures\n\nstruct CursorPosition: Codable { let offset: Int; let line: Int?; let column: Int? }\nstruct TextRange: Codable { let start: Int; let end: Int; let length: Int }\n\nstruct CursorContext: Codable {\n    let textBefore: String\n    let textAfter: String\n    let selectedText: String\n    let cursorPosition: CursorPosition\n    let selectionRange: TextRange?\n    let truncated: Bool\n    let totalLength: Int\n    let timestamp: String\n}\n\nstruct CursorContextResult: Codable {\n    let success: Bool\n    let context: CursorContext?\n    let error: String?\n    let method: String\n    let durationMs: Int\n}\n\n// MARK: - Utilities\n\nvar DEBUG_LOG = false\n@inline(__always) func dlog(_ s: String) { if DEBUG_LOG { fputs(s + \"\\n\", stderr) } }\n\n@inline(__always)\nfunc axErrorToString(_ e: AXError) -> String {\n    switch e {\n    case .success: return \"Success\"\n    case .failure: return \"Failure\"\n    case .illegalArgument: return \"IllegalArgument\"\n    case .invalidUIElement: return \"InvalidUIElement\"\n    case .invalidUIElementObserver: return \"InvalidUIElementObserver\"\n    case .cannotComplete: return \"CannotComplete\"\n    case .attributeUnsupported: return \"AttributeUnsupported\"\n    case .actionUnsupported: return \"ActionUnsupported\"\n    case .notificationUnsupported: return \"NotificationUnsupported\"\n    case .notImplemented: return \"NotImplemented\"\n    case .notificationAlreadyRegistered: return \"NotificationAlreadyRegistered\"\n    case .notificationNotRegistered: return \"NotificationNotRegistered\"\n    case .apiDisabled: return \"APIDisabled\"\n    case .noValue: return \"NoValue\"\n    case .parameterizedAttributeUnsupported: return \"ParameterizedAttributeUnsupported\"\n    case .notEnoughPrecision: return \"NotEnoughPrecision\"\n    @unknown default: return \"Unknown(\\(e.rawValue))\"\n    }\n}\n\n// Use CFTypeRef? so we control casts explicitly.\n@inline(__always)\nfunc axCopyAttr(_ element: AXUIElement, _ name: CFString) -> CFTypeRef? {\n    var v: CFTypeRef?\n    let r = AXUIElementCopyAttributeValue(element, name, &v)\n    return r == .success ? v : nil\n}\n\n@inline(__always)\nfunc axCopyParam(_ element: AXUIElement, _ name: CFString, _ param: CFTypeRef) -> CFTypeRef? {\n    var v: CFTypeRef?\n    let r = AXUIElementCopyParameterizedAttributeValue(element, name, param, &v)\n    return r == .success ? v : nil\n}\n\n@inline(__always)\nfunc axStringForRange(_ element: AXUIElement, location: Int, length: Int) -> String? {\n    var cfRange = CFRange(location: location, length: length)\n    guard let axRange = AXValueCreate(.cfRange, &cfRange) else { return nil }\n    return axCopyParam(element, CFs(kAXStringForRangeParameterizedAttribute as String), axRange) as? String\n}\n\n@inline(__always)\nfunc axStringForMarkerRange(_ element: AXUIElement, _ mr: CFTypeRef) -> String? {\n    axCopyParam(element, kAXStringForTextMarkerRangeParameterizedAttribute, mr) as? String\n}\n\n@inline(__always)\nfunc axLengthForMarkerRange(_ element: AXUIElement, _ mr: CFTypeRef) -> Int? {\n    (axCopyParam(element, kAXLengthForTextMarkerRangeParameterizedAttribute, mr) as? NSNumber)?.intValue\n}\n\n// Document marker range: attribute or parameterized\nfunc documentMarkerRange(_ element: AXUIElement) -> CFTypeRef? {\n    if let v = axCopyAttr(element, kAXDocumentRangeAttribute) { return v }\n    if let v = axCopyParam(element, kAXTextMarkerRangeForUIElementParameterizedAttribute, element) { return v }\n    return nil\n}\n\nfunc selectedMarkerRange(_ element: AXUIElement) -> CFTypeRef? {\n    axCopyAttr(element, kAXSelectedTextMarkerRangeAttribute)\n}\n\n// MARK: - Diagnostics\n\n// Inspect element in detail - shows ALL attributes\nfunc inspectElement(_ element: AXUIElement, label: String) {\n    guard DEBUG_LOG else { return }\n\n    fputs(\"\\n>>> INSPECTING: \\(label)\\n\", stderr)\n\n    // Get role\n    if let role = axCopyAttr(element, CFs(kAXRoleAttribute as String)) as? String {\n        fputs(\"    Role: \\(role)\\n\", stderr)\n    }\n\n    // Get all available attribute names\n    var attrNames: CFArray?\n    if AXUIElementCopyAttributeNames(element, &attrNames) == .success, let names = attrNames as? [String] {\n        fputs(\"    Available attributes (\\(names.count)):\\n\", stderr)\n        for name in names.prefix(20) {  // Limit to first 20 to avoid spam\n            fputs(\"      - \\(name)\\n\", stderr)\n        }\n        if names.count > 20 {\n            fputs(\"      ... (\\(names.count - 20) more)\\n\", stderr)\n        }\n\n        // Check interesting attributes for debugging\n        let interestingAttrs = [\"AXDescription\", \"AXTitle\", \"AXHelp\", \"AXPlaceholderValue\", \"ChromeAXNodeId\", \"AXDOMIdentifier\", \"AXDOMClassList\"]\n        fputs(\"    Checking interesting attributes:\\n\", stderr)\n        for attr in interestingAttrs {\n            if let value = axCopyAttr(element, attr as CFString) {\n                if let str = value as? String, !str.isEmpty {\n                    fputs(\"      \\(attr) = \\\"\\(str)\\\"\\n\", stderr)\n                } else if let num = value as? NSNumber {\n                    fputs(\"      \\(attr) = \\(num)\\n\", stderr)\n                }\n            }\n        }\n    }\n\n    // Check for children\n    if let childrenAny = axCopyAttr(element, CFs(kAXChildrenAttribute as String)),\n       let children = childrenAny as? [AXUIElement] {\n        fputs(\"    Children: \\(children.count)\\n\", stderr)\n        if children.count > 0 && children.count <= 5 {\n            for (i, child) in children.enumerated() {\n                if let role = axCopyAttr(child, CFs(kAXRoleAttribute as String)) as? String {\n                    fputs(\"      [\\(i)]: \\(role)\\n\", stderr)\n                }\n            }\n        } else if children.count > 5 {\n            fputs(\"      (first 5 of \\(children.count))\\n\", stderr)\n            for i in 0..<5 {\n                if let role = axCopyAttr(children[i], CFs(kAXRoleAttribute as String)) as? String {\n                    fputs(\"      [\\(i)]: \\(role)\\n\", stderr)\n                }\n            }\n        }\n    }\n\n    fputs(\"<<<\\n\\n\", stderr)\n}\n\n// Structured logging helpers\nfunc logMethodStart(_ method: String) {\n    dlog(\"[\\(method)] Attempting...\")\n}\n\nfunc logMethodSuccess(_ method: String, _ detail: String = \"\") {\n    let msg = detail.isEmpty ? \"✓ Success\" : \"✓ \\(detail)\"\n    dlog(\"[\\(method)] \\(msg)\")\n}\n\nfunc logMethodFailure(_ method: String, _ reason: String) {\n    dlog(\"[\\(method)] ✗ \\(reason)\")\n}\n\nfunc logMethodSkip(_ method: String, _ reason: String) {\n    dlog(\"[\\(method)] → Skipping: \\(reason)\")\n}\n\n// MARK: - Extraction Paths\n\n// Path A: Classic Cocoa (kAXValue + kAXSelectedTextRange)\nfunc valueBasedContext(_ element: AXUIElement, maxBefore: Int, maxAfter: Int, startTime: Date) -> CursorContextResult? {\n    logMethodStart(\"VALUE_METHOD\")\n\n    // Try to get AXValue\n    guard let fullText = axCopyAttr(element, CFs(kAXValueAttribute as String)) as? String else {\n        logMethodFailure(\"VALUE_METHOD\", \"AXValue attribute not available or not a string\")\n        logMethodSkip(\"VALUE_METHOD\", \"no value attribute\")\n        return nil\n    }\n\n    guard !fullText.isEmpty && fullText.count > 0 else {\n        logMethodFailure(\"VALUE_METHOD\", \"AXValue is empty (length: \\(fullText.count))\")\n        logMethodSkip(\"VALUE_METHOD\", \"value is empty\")\n        return nil\n    }\n\n    logMethodSuccess(\"VALUE_METHOD\", \"Got AXValue with \\(fullText.count) characters\")\n\n    let selectedText = (axCopyAttr(element, CFs(kAXSelectedTextAttribute as String)) as? String) ?? \"\"\n    var cursorOffset = 0\n    var selection: TextRange? = nil\n\n    if let any = axCopyAttr(element, CFs(kAXSelectedTextRangeAttribute as String)) {\n        let axv = any as! AXValue // CFTypeRef → AXValue (CoreFoundation type; explicit cast)\n        var cfRange = CFRange(location: 0, length: 0)\n        if AXValueGetValue(axv, .cfRange, &cfRange) {\n            cursorOffset = cfRange.location\n            if cfRange.length > 0 {\n                selection = TextRange(start: cfRange.location,\n                                      end: cfRange.location + cfRange.length,\n                                      length: cfRange.length)\n            }\n            logMethodSuccess(\"VALUE_METHOD\", \"Got cursor position at offset \\(cursorOffset)\")\n        }\n    } else {\n        dlog(\"[VALUE_METHOD] No AXSelectedTextRange (cursor will default to 0)\")\n    }\n\n    let totalLength = fullText.count\n    let startOffset = max(0, cursorOffset - maxBefore)\n    let endOffset = min(totalLength, cursorOffset + (selection?.length ?? 0) + maxAfter)\n\n    let startIndex = fullText.index(fullText.startIndex, offsetBy: startOffset)\n    let cursorIndex = fullText.index(fullText.startIndex, offsetBy: cursorOffset)\n    let endIndex = fullText.index(fullText.startIndex, offsetBy: endOffset)\n\n    let textBefore = String(fullText[startIndex..<cursorIndex])\n    let textAfter = String(fullText[cursorIndex..<endIndex])\n    let truncated = (startOffset > 0) || (endOffset < totalLength)\n\n    logMethodSuccess(\"VALUE_METHOD\", \"Returning context (before: \\(textBefore.count), after: \\(textAfter.count))\")\n\n    let context = CursorContext(\n        textBefore: textBefore,\n        textAfter: textAfter,\n        selectedText: selectedText,\n        cursorPosition: CursorPosition(offset: cursorOffset, line: nil, column: nil),\n        selectionRange: selection,\n        truncated: truncated,\n        totalLength: totalLength,\n        timestamp: ISO8601DateFormatter().string(from: Date())\n    )\n    let elapsed = Int(Date().timeIntervalSince(startTime) * 1000)\n    return CursorContextResult(success: true, context: context, error: nil, method: \"accessibility:value\", durationMs: elapsed)\n}\n\n// Path B: Marker-based (Chromium/WebKit/Electron).\nstruct MarkerContext {\n    let fullText: String\n    let before: String\n    let after: String\n    let selected: String\n    let cursorOffset: Int\n    let selectionLength: Int\n    let totalLength: Int\n}\n\nfunc markerBasedContext(_ element: AXUIElement, maxBefore: Int, maxAfter: Int) -> MarkerContext? {\n    // Try to get document marker range\n    guard let docMR_any = documentMarkerRange(element) else {\n        logMethodFailure(\"MARKER_METHOD\", \"No document marker range available\")\n        return nil\n    }\n    logMethodSuccess(\"MARKER_METHOD\", \"Got document marker range\")\n\n    // Try to get full text from document range\n    guard let fullText = axStringForMarkerRange(element, docMR_any) else {\n        logMethodFailure(\"MARKER_METHOD\", \"Could not extract text from document marker range\")\n        return nil\n    }\n\n    guard let docLen = axLengthForMarkerRange(element, docMR_any) else {\n        logMethodFailure(\"MARKER_METHOD\", \"Could not get length for document marker range\")\n        return nil\n    }\n\n    // Reject empty text - this means the API is available but not actually providing content\n    guard docLen > 0 && !fullText.isEmpty else {\n        logMethodFailure(\"MARKER_METHOD\", \"Document marker range returned empty text (length: \\(docLen))\")\n        return nil\n    }\n\n    logMethodSuccess(\"MARKER_METHOD\", \"Got document text with \\(docLen) characters\")\n\n    // Try to get selected marker range\n    guard let selMR_any = selectedMarkerRange(element) else {\n        logMethodFailure(\"MARKER_METHOD\", \"No selected marker range available\")\n        return nil\n    }\n    logMethodSuccess(\"MARKER_METHOD\", \"Got selected marker range\")\n\n    // CFTypeRef → AXTextMarkerRange (explicit casts)\n    let docMR = docMR_any as! AXTextMarkerRange\n    let selMR = selMR_any as! AXTextMarkerRange\n\n    // Extract start markers\n    let docStartMarker: AXTextMarker = AXTextMarkerRangeCopyStartMarker(docMR)\n    let selStartMarker: AXTextMarker = AXTextMarkerRangeCopyStartMarker(selMR)\n    // (selEnd available if needed)\n    _ = AXTextMarkerRangeCopyEndMarker(selMR)\n\n    // Build [docStart, selStart) to measure caret offset (returns non-optional)\n    let startToSelStart: AXTextMarkerRange = AXTextMarkerRangeCreate(kCFAllocatorDefault, docStartMarker, selStartMarker)\n    let cursorOffset = axLengthForMarkerRange(element, startToSelStart) ?? 0\n\n    let selectedText = axStringForMarkerRange(element, selMR) ?? \"\"\n    let selLen = axLengthForMarkerRange(element, selMR) ?? 0\n\n    logMethodSuccess(\"MARKER_METHOD\", \"Calculated cursor offset: \\(cursorOffset), selection length: \\(selLen)\")\n\n    // Window around caret in Swift\n    let safeCursor = max(0, min(cursorOffset, docLen))\n    let afterEnd = min(docLen, safeCursor + selLen + maxAfter)\n    let beforeStart = max(0, safeCursor - maxBefore)\n\n    let startIdx = fullText.startIndex\n    let beforeStartIdx = fullText.index(startIdx, offsetBy: beforeStart)\n    let cursorIdx = fullText.index(startIdx, offsetBy: safeCursor)\n    let afterEndIdx = fullText.index(startIdx, offsetBy: afterEnd)\n\n    let before = String(fullText[beforeStartIdx..<cursorIdx])\n    let after = String(fullText[cursorIdx..<afterEndIdx])\n\n    return MarkerContext(\n        fullText: fullText,\n        before: before,\n        after: after,\n        selected: selectedText,\n        cursorOffset: safeCursor,\n        selectionLength: selLen,\n        totalLength: docLen\n    )\n}\n\n// Path C: Range-based fallback using kAXStringForRange (works in some wrappers)\nfunc rangeBasedContext(_ element: AXUIElement, maxBefore: Int, maxAfter: Int, startTime: Date) -> CursorContextResult? {\n    let bigLen = 5_000_000\n\n    guard let fullText = axStringForRange(element, location: 0, length: bigLen) else {\n        logMethodFailure(\"RANGE_METHOD\", \"AXStringForRange not supported or failed\")\n        return nil\n    }\n\n    guard !fullText.isEmpty && fullText.count > 0 else {\n        logMethodFailure(\"RANGE_METHOD\", \"AXStringForRange returned empty text (length: \\(fullText.count))\")\n        return nil\n    }\n\n    logMethodSuccess(\"RANGE_METHOD\", \"Got text via AXStringForRange (\\(fullText.count) characters)\")\n\n    var cursorOffset = 0\n    var selLen = 0\n    if let any = axCopyAttr(element, CFs(kAXSelectedTextRangeAttribute as String)) {\n        let axv = any as! AXValue\n        var cfRange = CFRange(location: 0, length: 0)\n        if AXValueGetValue(axv, .cfRange, &cfRange) {\n            cursorOffset = cfRange.location\n            selLen = cfRange.length\n            logMethodSuccess(\"RANGE_METHOD\", \"Got cursor position at offset \\(cursorOffset)\")\n        }\n    } else {\n        dlog(\"[RANGE_METHOD] No AXSelectedTextRange (cursor will default to 0)\")\n    }\n\n    let totalLength = fullText.count\n    let startOffset = max(0, min(cursorOffset, totalLength) - maxBefore)\n    let endOffset = min(totalLength, cursorOffset + selLen + maxAfter)\n\n    let startIndex = fullText.index(fullText.startIndex, offsetBy: startOffset)\n    let cursorIndex = fullText.index(fullText.startIndex, offsetBy: min(cursorOffset, totalLength))\n    let endIndex = fullText.index(fullText.startIndex, offsetBy: endOffset)\n\n    let textBefore = String(fullText[startIndex..<cursorIndex])\n    let textAfter = String(fullText[cursorIndex..<endIndex])\n    let truncated = (startOffset > 0) || (endOffset < totalLength)\n\n    let selectedText: String = selLen > 0 ? (axStringForRange(element, location: cursorOffset, length: selLen) ?? \"\") : \"\"\n\n    let selectionRange: TextRange? = selLen > 0\n        ? TextRange(start: cursorOffset, end: cursorOffset + selLen, length: selLen)\n        : nil\n\n    logMethodSuccess(\"RANGE_METHOD\", \"Returning context (before: \\(textBefore.count), after: \\(textAfter.count))\")\n\n    let context = CursorContext(\n        textBefore: textBefore,\n        textAfter: textAfter,\n        selectedText: selectedText,\n        cursorPosition: CursorPosition(offset: min(cursorOffset, totalLength), line: nil, column: nil),\n        selectionRange: selectionRange,\n        truncated: truncated,\n        totalLength: totalLength,\n        timestamp: ISO8601DateFormatter().string(from: Date())\n    )\n    let elapsed = Int(Date().timeIntervalSince(startTime) * 1000)\n    return CursorContextResult(success: true, context: context, error: nil, method: \"accessibility:range\", durationMs: elapsed)\n}\n\n// Try focused element, then a single parent hop\nfunc bestRangeContextWithParentHop(_ element: AXUIElement, maxBefore: Int, maxAfter: Int, startTime: Date) -> CursorContextResult? {\n    logMethodStart(\"RANGE_METHOD\")\n    dlog(\"[RANGE_METHOD] Trying on focused element...\")\n\n    if let r = rangeBasedContext(element, maxBefore: maxBefore, maxAfter: maxAfter, startTime: startTime) {\n        logMethodSuccess(\"RANGE_METHOD\", \"Succeeded on focused element\")\n        return r\n    }\n\n    // Check if focused element is a text role - if so, skip parent to avoid UI chrome\n    let textRoles = [\"AXTextArea\", \"AXTextField\", \"AXWebArea\"]\n    if let role = axCopyAttr(element, CFs(kAXRoleAttribute as String)) as? String {\n        if textRoles.contains(role) {\n            dlog(\"[RANGE_METHOD] Focused element is \\(role) but has no text - skipping parent to avoid UI chrome\")\n            logMethodSkip(\"RANGE_METHOD\", \"text role element with no content (will try tree traversal)\")\n            return nil\n        }\n    }\n\n    dlog(\"[RANGE_METHOD] Failed on focused element, trying parent...\")\n    if let parentAny = axCopyAttr(element, CFs(kAXParentAttribute as String)) {\n        let parent = parentAny as! AXUIElement\n        if let r = rangeBasedContext(parent, maxBefore: maxBefore, maxAfter: maxAfter, startTime: startTime) {\n            logMethodSuccess(\"RANGE_METHOD\", \"Succeeded on parent element\")\n            return r\n        }\n        dlog(\"[RANGE_METHOD] Failed on parent element too\")\n    } else {\n        dlog(\"[RANGE_METHOD] No parent element available\")\n    }\n\n    logMethodSkip(\"RANGE_METHOD\", \"range API not available on element or parent\")\n    return nil\n}\n\nfunc bestMarkerContextWithParentHop(_ element: AXUIElement, maxBefore: Int, maxAfter: Int) -> (MarkerContext, String)? {\n    logMethodStart(\"MARKER_METHOD\")\n    dlog(\"[MARKER_METHOD] Trying on focused element...\")\n\n    if let m = markerBasedContext(element, maxBefore: maxBefore, maxAfter: maxAfter) {\n        logMethodSuccess(\"MARKER_METHOD\", \"Succeeded on focused element\")\n        return (m, \"accessibility:marker\")\n    }\n\n    // Check if focused element is a text role (e.g., AXTextArea, AXTextField)\n    // If so, don't try parent - it likely contains UI chrome\n    let textRoles = [\"AXTextArea\", \"AXTextField\", \"AXWebArea\"]\n    if let role = axCopyAttr(element, CFs(kAXRoleAttribute as String)) as? String {\n        if textRoles.contains(role) {\n            dlog(\"[MARKER_METHOD] Focused element is \\(role) but has no text - skipping parent to avoid UI chrome\")\n            logMethodSkip(\"MARKER_METHOD\", \"text role element with no content (will try tree traversal)\")\n            return nil\n        }\n    }\n\n    dlog(\"[MARKER_METHOD] Failed on focused element, trying parent...\")\n    if let parentAny = axCopyAttr(element, CFs(kAXParentAttribute as String)) {\n        let parent = parentAny as! AXUIElement\n        if let m = markerBasedContext(parent, maxBefore: maxBefore, maxAfter: maxAfter) {\n            logMethodSuccess(\"MARKER_METHOD\", \"Succeeded on parent element\")\n            return (m, \"accessibility:marker(parent)\")\n        }\n        dlog(\"[MARKER_METHOD] Failed on parent element too\")\n    } else {\n        dlog(\"[MARKER_METHOD] No parent element available\")\n    }\n\n    logMethodSkip(\"MARKER_METHOD\", \"marker API not available on element or parent\")\n    return nil\n}\n\n// MARK: - Path D: Electron Tree Traversal\n\n// Recursively search for text-capable elements in the accessibility tree\nfunc findTextElements(_ element: AXUIElement, maxDepth: Int, currentDepth: Int = 0) -> [AXUIElement] {\n    var results: [AXUIElement] = []\n\n    if currentDepth >= maxDepth {\n        return results\n    }\n\n    // Check if current element might have text content\n    let textRoles = [\"AXTextArea\", \"AXTextField\", \"AXWebArea\", \"AXGroup\", \"AXScrollArea\"]\n    if let role = axCopyAttr(element, CFs(kAXRoleAttribute as String)) as? String {\n        if textRoles.contains(role) {\n            // Check if it has any text-related attributes\n            let hasValue = axCopyAttr(element, CFs(kAXValueAttribute as String)) != nil\n            let hasMarkerRange = documentMarkerRange(element) != nil\n            let hasNumberOfChars = axCopyAttr(element, CFs(kAXNumberOfCharactersAttribute as String)) != nil\n\n            if hasValue || hasMarkerRange || hasNumberOfChars {\n                results.append(element)\n                dlog(\"[TREE_TRAVERSAL] Found potential text element: \\(role)\")\n            }\n        }\n    }\n\n    // Search children\n    if let childrenAny = axCopyAttr(element, CFs(kAXChildrenAttribute as String)),\n       let children = childrenAny as? [AXUIElement] {\n        for child in children {\n            results.append(contentsOf: findTextElements(child, maxDepth: maxDepth, currentDepth: currentDepth + 1))\n        }\n    }\n\n    return results\n}\n\n// Try extraction methods on a candidate element\nfunc tryExtractFromElement(_ element: AXUIElement, maxBefore: Int, maxAfter: Int, startTime: Date) -> CursorContextResult? {\n    // Try value-based first\n    if let result = valueBasedContext(element, maxBefore: maxBefore, maxAfter: maxAfter, startTime: startTime) {\n        return result\n    }\n\n    // Try marker-based (without parent hop, we're already traversing)\n    if let mc = markerBasedContext(element, maxBefore: maxBefore, maxAfter: maxAfter) {\n        let truncated = (mc.cursorOffset > maxBefore) || (mc.cursorOffset + mc.selectionLength + maxAfter < mc.totalLength)\n        let selRange: TextRange? = mc.selectionLength > 0\n            ? TextRange(start: mc.cursorOffset, end: mc.cursorOffset + mc.selectionLength, length: mc.selectionLength)\n            : nil\n\n        let context = CursorContext(\n            textBefore: mc.before,\n            textAfter: mc.after,\n            selectedText: mc.selected,\n            cursorPosition: CursorPosition(offset: mc.cursorOffset, line: nil, column: nil),\n            selectionRange: selRange,\n            truncated: truncated,\n            totalLength: mc.totalLength,\n            timestamp: ISO8601DateFormatter().string(from: Date())\n        )\n        let elapsed = Int(Date().timeIntervalSince(startTime) * 1000)\n        return CursorContextResult(success: true, context: context, error: nil, method: \"accessibility:marker(tree)\", durationMs: elapsed)\n    }\n\n    // Try range-based\n    if let result = rangeBasedContext(element, maxBefore: maxBefore, maxAfter: maxAfter, startTime: startTime) {\n        return result\n    }\n\n    return nil\n}\n\n// Path D: Electron/Chromium tree traversal fallback\nfunc electronTreeTraversal(_ focusedElement: AXUIElement, maxBefore: Int, maxAfter: Int, startTime: Date) -> CursorContextResult? {\n    logMethodStart(\"TREE_TRAVERSAL\")\n    dlog(\"[TREE_TRAVERSAL] Searching descendants for text elements (Electron fallback)...\")\n\n    inspectElement(focusedElement, label: \"Focused Element (Tree Traversal)\")\n\n    // Search children up to 4 levels deep\n    let candidates = findTextElements(focusedElement, maxDepth: 4)\n    dlog(\"[TREE_TRAVERSAL] Found \\(candidates.count) candidate text element(s)\")\n\n    // Try extraction on each candidate\n    for (index, candidate) in candidates.enumerated() {\n        if let role = axCopyAttr(candidate, CFs(kAXRoleAttribute as String)) as? String {\n            dlog(\"[TREE_TRAVERSAL] Trying candidate \\(index + 1)/\\(candidates.count): \\(role)\")\n        }\n\n        if let result = tryExtractFromElement(candidate, maxBefore: maxBefore, maxAfter: maxAfter, startTime: startTime) {\n            logMethodSuccess(\"TREE_TRAVERSAL\", \"Found text in descendant element \\(index + 1)\")\n            return result\n        }\n    }\n\n    // Also try parent traversal (go up instead of down)\n    dlog(\"[TREE_TRAVERSAL] No success in descendants, trying ancestors...\")\n    var currentElement = focusedElement\n    for level in 1...3 {\n        guard let parentAny = axCopyAttr(currentElement, CFs(kAXParentAttribute as String)) else {\n            dlog(\"[TREE_TRAVERSAL] No more ancestors at level \\(level)\")\n            break\n        }\n\n        let parent = parentAny as! AXUIElement\n        if let role = axCopyAttr(parent, CFs(kAXRoleAttribute as String)) as? String {\n            dlog(\"[TREE_TRAVERSAL] Trying ancestor at level \\(level): \\(role)\")\n        }\n\n        if let result = tryExtractFromElement(parent, maxBefore: maxBefore, maxAfter: maxAfter, startTime: startTime) {\n            logMethodSuccess(\"TREE_TRAVERSAL\", \"Found text in ancestor at level \\(level)\")\n            return result\n        }\n\n        currentElement = parent\n    }\n\n    logMethodSkip(\"TREE_TRAVERSAL\", \"no text found in tree traversal\")\n    return nil\n}\n\n// MARK: - Core\n\nfunc getCursorContext(maxCharsBefore: Int, maxCharsAfter: Int) -> CursorContextResult {\n    let startTime = Date()\n\n    guard let frontmostApp = NSWorkspace.shared.frontmostApplication else {\n        let elapsed = Int(Date().timeIntervalSince(startTime) * 1000)\n        return CursorContextResult(success: false, context: nil, error: \"No frontmost application\", method: \"accessibility\", durationMs: elapsed)\n    }\n    let pid = frontmostApp.processIdentifier\n    let appElement = AXUIElementCreateApplication(pid)\n\n    // Help Chromium/Electron\n    AXUIElementSetAttributeValue(appElement, \"AXEnhancedUserInterface\" as CFString, kCFBooleanTrue)\n    AXUIElementSetAttributeValue(appElement, \"AXManualAccessibility\" as CFString, kCFBooleanTrue)\n    dlog(\"✅ Enabled AXEnhancedUserInterface and AXManualAccessibility\")\n    dlog(\"🔍 Focused Application: \\(frontmostApp.localizedName ?? \"Unknown\")\")\n\n    // Trigger lazy initialization of accessibility tree by reading the role attribute\n    // This ensures the accessibility hierarchy is built before we query for focused element\n    // See: https://stackoverflow.com/questions/77954521\n    let _ = axCopyAttr(appElement, CFs(kAXRoleAttribute as String))\n    dlog(\"🔧 Triggered accessibility tree initialization via kAXRoleAttribute\")\n\n    // Poll for focused element up to 5 times with 10ms delays\n    // Handles lazy tree building in Chromium/Electron apps\n    var focusedElementObj: CFTypeRef?\n    var r: AXError = .failure\n\n    for attempt in 1...5 {\n        r = AXUIElementCopyAttributeValue(appElement, CFs(kAXFocusedUIElementAttribute as String), &focusedElementObj)\n\n        if r == .success && focusedElementObj != nil {\n            dlog(\"✅ Got focused element on attempt \\(attempt)\")\n            break\n        }\n\n        if attempt < 5 {\n            Thread.sleep(forTimeInterval: 0.01)  // 10ms\n            dlog(\"⏱️  Attempt \\(attempt) failed, waiting 10ms...\")\n        }\n    }\n\n    // If we got a focused element, try the standard extraction methods\n    if r == .success, let focusedAny = focusedElementObj {\n        let element = focusedAny as! AXUIElement\n\n        // 1) Value-based\n        if let v = valueBasedContext(element, maxBefore: maxCharsBefore, maxAfter: maxCharsAfter, startTime: startTime) { return v }\n\n        // 2) Marker-based (+ parent hop)\n        if let (mc, methodTag) = bestMarkerContextWithParentHop(element, maxBefore: maxCharsBefore, maxAfter: maxCharsAfter) {\n        let truncated = (mc.cursorOffset > maxCharsBefore) || (mc.cursorOffset + mc.selectionLength + maxCharsAfter < mc.totalLength)\n        let selRange: TextRange? = mc.selectionLength > 0\n            ? TextRange(start: mc.cursorOffset, end: mc.cursorOffset + mc.selectionLength, length: mc.selectionLength)\n            : nil\n\n        let context = CursorContext(\n            textBefore: mc.before,\n            textAfter: mc.after,\n            selectedText: mc.selected,\n            cursorPosition: CursorPosition(offset: mc.cursorOffset, line: nil, column: nil),\n            selectionRange: selRange,\n            truncated: truncated,\n            totalLength: mc.totalLength,\n            timestamp: ISO8601DateFormatter().string(from: Date())\n        )\n        let elapsed = Int(Date().timeIntervalSince(startTime) * 1000)\n        return CursorContextResult(success: true, context: context, error: nil, method: methodTag, durationMs: elapsed)\n    }\n\n        // 3) Range-based fallback (+ parent hop)\n        if let r = bestRangeContextWithParentHop(element, maxBefore: maxCharsBefore, maxAfter: maxCharsAfter, startTime: startTime) { return r }\n\n        // 4) Electron tree traversal (comprehensive fallback)\n        dlog(\"\\n⚠️  Standard methods failed, trying Electron tree traversal...\")\n        if let r = electronTreeTraversal(element, maxBefore: maxCharsBefore, maxAfter: maxCharsAfter, startTime: startTime) { return r }\n    } else {\n        let errorDetail = \"AXError: \\(axErrorToString(r)), element nil: \\(focusedElementObj == nil)\"\n        dlog(\"❌ Failed to get focused element after 5 attempts: \\(errorDetail)\")\n    }\n\n    let elapsed = Int(Date().timeIntervalSince(startTime) * 1000)\n    return CursorContextResult(success: false, context: nil, error: \"Unable to retrieve text via Value, Marker, Range, or Tree Traversal\", method: \"accessibility\", durationMs: elapsed)\n}\n\n// MARK: - CLI Entry Point\n\nCursorContextCLI.main()\n"
  },
  {
    "path": "native/global-key-listener/Cargo.toml",
    "content": "[package]\nname = \"global-key-listener\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nserde_json = \"1.0\"\nchrono = { version = \"0.4\", features = [\"serde\"] }\nserde = { version = \"1.0\", features = [\"derive\"] }\n\n[dependencies.rdev]\ngit = \"https://github.com/heyito/rdev\"\nbranch = \"main\"\nfeatures = [\"unstable_grab\"]\n\n[target.'cfg(target_os = \"linux\")'.dependencies]\nx11 = { version = \"2.21.0\", features = [\"xlib\"] }\nevdev = \"0.12.1\"\n\n[target.'cfg(target_os = \"windows\")'.dependencies]\nwinapi = { version = \"0.3\", features = [\"winuser\"] }\n\n[target.'cfg(target_os = \"macos\")'.dependencies]\ncore-foundation = \"0.9\"\ncore-graphics = \"0.22\"\ncocoa = \"0.25\"\nobjc = \"0.2\"\n\n[build-dependencies]\ntauri-winres = \"0.3.5\"\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "native/global-key-listener/README.md",
    "content": "# Global Key Listener\n\nA Rust-based global keyboard event listener that captures and blocks keyboard events, designed for integration with Electron applications.\n\n## Features\n\n- Captures global key press and key release events\n- Outputs events as JSON with timestamps and raw key codes\n- Supports blocking/unblocking specific keys\n- Cross-platform support (Windows, macOS, Linux)\n- Special handling for modifier keys (including fn key on macOS)\n- Command interface for runtime control\n\n## Usage\n\nThe key listener can be controlled through stdin commands in JSON format:\n\n```json\n// Block specific keys\n{\"command\": \"block\", \"keys\": [\"KeyA\", \"KeyB\", \"KeyC\"]}\n\n// Unblock a specific key\n{\"command\": \"unblock\", \"key\": \"KeyA\"}\n\n// Get list of currently blocked keys\n{\"command\": \"get_blocked\"}\n```\n\nEvents are output to stdout in JSON format:\n\n```json\n{\n  \"type\": \"keydown\",\n  \"key\": \"KeyA\",\n  \"timestamp\": \"2024-06-14T01:58:44.617Z\",\n  \"raw_code\": 65\n}\n```\n\n## Requirements\n\n### macOS\n\nYou'll need to grant accessibility permissions to your terminal or the compiled binary:\n\n1. Go to System Preferences → Security & Privacy → Privacy → Accessibility\n2. Add your terminal application or the compiled binary to the list\n\n### Linux\n\nYou may need to run with elevated privileges:\n\n```bash\nsudo cargo run\n```\n\n### Windows\n\nShould work without additional permissions.\n\n## Building\n\n```bash\ncargo build --release\n```\n\nThe binary will be available at `target/release/global-key-listener`\n\n## Integration with Electron\n\nWhen integrating with Electron:\n\n1. Copy the compiled binary to your app's resources directory\n2. Use the binary path from `process.resourcesPath` in production\n3. Use the development path (`target/release/global-key-listener`) during development\n4. Ensure proper error handling and process management in your Electron app\n\n## Key Names\n\nThe key listener uses standard key names that match the `rdev` library's Key enum. Common examples:\n\n- `KeyA` through `KeyZ` for letter keys\n- `Digit1` through `Digit9` for number keys\n- `Function` for the fn key (macOS)\n- `ShiftLeft`, `ShiftRight` for modifier keys\n- `Space`, `Enter`, `Escape` for special keys\n\n## Notes\n\n- This captures ALL keyboard input globally, so use responsibly\n- The program will run until terminated with Ctrl+C\n- Some special keys might not have raw codes assigned\n- Performance is generally good, but capturing every keystroke does use some CPU\n"
  },
  {
    "path": "native/global-key-listener/build.rs",
    "content": "fn main() {\n    #[cfg(target_os = \"windows\")]\n    {\n        let mut res = tauri_winres::WindowsResource::new();\n\n        // Set all metadata\n        res.set_manifest_file(\"global-key-listener.manifest\");\n        res.set(\"FileDescription\", \"Keyboard Event Monitor - Global keyboard listener for accessibility and productivity applications\");\n        res.set(\"ProductName\", \"Global Key Listener - Accessibility Tool\");\n        res.set(\"CompanyName\", \"Demox Labs\");\n        res.set(\n            \"LegalCopyright\",\n            \"Copyright © 2025 Demox Labs. All rights reserved.\",\n        );\n        res.set(\"FileVersion\", \"0.1.0.0\");\n        res.set(\"ProductVersion\", \"0.1.0.0\");\n        res.set(\"InternalName\", \"global-key-listener\");\n        res.set(\"OriginalFilename\", \"global-key-listener.exe\");\n        res.set(\n            \"Comments\",\n            \"Accessibility utility for global keyboard event monitoring via command-line interface\",\n        );\n\n        res.compile().unwrap();\n    }\n}\n"
  },
  {
    "path": "native/global-key-listener/global-key-listener.manifest",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\">\n  <assemblyIdentity\n    version=\"0.1.0.0\"\n    processorArchitecture=\"*\"\n    name=\"DemoxLabs.GlobalKeyListener.AccessibilityTool\"\n    type=\"win32\"\n  />\n  <description>Accessibility keyboard event monitoring utility for enhanced productivity</description>\n\n  <!-- Execution Level -->\n  <trustInfo xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <security>\n      <requestedPrivileges>\n        <requestedExecutionLevel level=\"asInvoker\" uiAccess=\"false\" />\n      </requestedPrivileges>\n    </security>\n  </trustInfo>\n\n  <!-- Windows 10/11 Compatibility -->\n  <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n    <application>\n      <!-- Windows 10+ -->\n      <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\"/>\n    </application>\n  </compatibility>\n\n  <!-- DPI Awareness -->\n  <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <windowsSettings>\n      <dpiAware xmlns=\"http://schemas.microsoft.com/SMI/2005/WindowsSettings\">true</dpiAware>\n      <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2</dpiAwareness>\n    </windowsSettings>\n  </application>\n</assembly>\n"
  },
  {
    "path": "native/global-key-listener/src/key_codes.rs",
    "content": "use rdev::Key;\n\n/// Maps a Key enum variant to its corresponding key code\npub fn key_to_code(key: &Key) -> Option<u32> {\n    match key {\n        Key::Alt => Some(18),\n        Key::AltGr => Some(225),\n        Key::Backspace => Some(8),\n        Key::CapsLock => Some(20),\n        Key::ControlLeft => Some(17),\n        Key::ControlRight => Some(17),\n        Key::Delete => Some(46),\n        Key::DownArrow => Some(40),\n        Key::End => Some(35),\n        Key::Escape => Some(27),\n        Key::F1 => Some(112),\n        Key::F2 => Some(113),\n        Key::F3 => Some(114),\n        Key::F4 => Some(115),\n        Key::F5 => Some(116),\n        Key::F6 => Some(117),\n        Key::F7 => Some(118),\n        Key::F8 => Some(119),\n        Key::F9 => Some(120),\n        Key::F10 => Some(121),\n        Key::F11 => Some(122),\n        Key::F12 => Some(123),\n        Key::Home => Some(36),\n        Key::LeftArrow => Some(37),\n        Key::MetaLeft => Some(91),\n        Key::MetaRight => Some(92),\n        Key::PageDown => Some(34),\n        Key::PageUp => Some(33),\n        Key::Return => Some(13),\n        Key::RightArrow => Some(39),\n        Key::ShiftLeft => Some(16),\n        Key::ShiftRight => Some(16),\n        Key::Space => Some(32),\n        Key::Tab => Some(9),\n        Key::UpArrow => Some(38),\n        Key::PrintScreen => Some(44),\n        Key::ScrollLock => Some(145),\n        Key::Pause => Some(19),\n        Key::NumLock => Some(144),\n        Key::BackQuote => Some(192),\n        Key::Num1 => Some(49),\n        Key::Num2 => Some(50),\n        Key::Num3 => Some(51),\n        Key::Num4 => Some(52),\n        Key::Num5 => Some(53),\n        Key::Num6 => Some(54),\n        Key::Num7 => Some(55),\n        Key::Num8 => Some(56),\n        Key::Num9 => Some(57),\n        Key::Num0 => Some(48),\n        Key::Minus => Some(189),\n        Key::Equal => Some(187),\n        Key::KeyQ => Some(81),\n        Key::KeyW => Some(87),\n        Key::KeyE => Some(69),\n        Key::KeyR => Some(82),\n        Key::KeyT => Some(84),\n        Key::KeyY => Some(89),\n        Key::KeyU => Some(85),\n        Key::KeyI => Some(73),\n        Key::KeyO => Some(79),\n        Key::KeyP => Some(80),\n        Key::LeftBracket => Some(219),\n        Key::RightBracket => Some(221),\n        Key::KeyA => Some(65),\n        Key::KeyS => Some(83),\n        Key::KeyD => Some(68),\n        Key::KeyF => Some(70),\n        Key::KeyG => Some(71),\n        Key::KeyH => Some(72),\n        Key::KeyJ => Some(74),\n        Key::KeyK => Some(75),\n        Key::KeyL => Some(76),\n        Key::SemiColon => Some(186),\n        Key::Quote => Some(222),\n        Key::BackSlash => Some(220),\n        Key::IntlBackslash => Some(226),\n        Key::KeyZ => Some(90),\n        Key::KeyX => Some(88),\n        Key::KeyC => Some(67),\n        Key::KeyV => Some(86),\n        Key::KeyB => Some(66),\n        Key::KeyN => Some(78),\n        Key::KeyM => Some(77),\n        Key::Comma => Some(188),\n        Key::Dot => Some(190),\n        Key::Slash => Some(191),\n        Key::Function => Some(179),\n        _ => None, // For keys that don't have a standard code\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_key_to_code_letters() {\n        // Test common letter keys\n        assert_eq!(key_to_code(&Key::KeyA), Some(65));\n        assert_eq!(key_to_code(&Key::KeyZ), Some(90));\n        assert_eq!(key_to_code(&Key::KeyC), Some(67));\n    }\n\n    #[test]\n    fn test_key_to_code_numbers() {\n        // Test number keys\n        assert_eq!(key_to_code(&Key::Num0), Some(48));\n        assert_eq!(key_to_code(&Key::Num5), Some(53));\n        assert_eq!(key_to_code(&Key::Num9), Some(57));\n    }\n\n    #[test]\n    fn test_key_to_code_modifiers() {\n        // Test modifier keys\n        assert_eq!(key_to_code(&Key::ControlLeft), Some(17));\n        assert_eq!(key_to_code(&Key::ControlRight), Some(17));\n        assert_eq!(key_to_code(&Key::ShiftLeft), Some(16));\n        assert_eq!(key_to_code(&Key::ShiftRight), Some(16));\n        assert_eq!(key_to_code(&Key::Alt), Some(18));\n    }\n\n    #[test]\n    fn test_key_to_code_function_keys() {\n        // Test function keys\n        assert_eq!(key_to_code(&Key::F1), Some(112));\n        assert_eq!(key_to_code(&Key::F12), Some(123));\n        assert_eq!(key_to_code(&Key::Function), Some(179));\n    }\n\n    #[test]\n    fn test_key_to_code_special_keys() {\n        // Test special keys\n        assert_eq!(key_to_code(&Key::Escape), Some(27));\n        assert_eq!(key_to_code(&Key::Return), Some(13));\n        assert_eq!(key_to_code(&Key::Space), Some(32));\n        assert_eq!(key_to_code(&Key::Tab), Some(9));\n        assert_eq!(key_to_code(&Key::Backspace), Some(8));\n    }\n\n    #[test]\n    fn test_key_to_code_arrow_keys() {\n        // Test arrow keys\n        assert_eq!(key_to_code(&Key::UpArrow), Some(38));\n        assert_eq!(key_to_code(&Key::DownArrow), Some(40));\n        assert_eq!(key_to_code(&Key::LeftArrow), Some(37));\n        assert_eq!(key_to_code(&Key::RightArrow), Some(39));\n    }\n}\n"
  },
  {
    "path": "native/global-key-listener/src/main.rs",
    "content": "use chrono::Utc;\n#[cfg(target_os = \"windows\")]\nuse rdev::{grab, simulate, Event, EventType, Key};\n#[cfg(not(target_os = \"windows\"))]\nuse rdev::{grab, Event, EventType, Key};\nuse serde::{Deserialize, Serialize};\nuse serde_json::json;\nuse std::io::{self, BufRead, Write};\nuse std::thread;\nuse std::time::Duration;\n\nmod key_codes;\n\n#[cfg(target_os = \"macos\")]\nuse cocoa::base::{id, nil};\n#[cfg(target_os = \"macos\")]\nuse cocoa::foundation::{NSProcessInfo, NSString};\n#[cfg(target_os = \"macos\")]\nuse objc::{msg_send, sel, sel_impl};\n\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\nstruct HotkeyCombo {\n    keys: Vec<String>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\n#[serde(tag = \"command\")]\nenum Command {\n    #[serde(rename = \"register_hotkeys\")]\n    RegisterHotkeys { hotkeys: Vec<HotkeyCombo> },\n}\n\n// Global state for registered hotkeys and currently pressed keys\n#[allow(static_mut_refs)]\nstatic mut REGISTERED_HOTKEYS: Vec<HotkeyCombo> = Vec::new();\n#[allow(static_mut_refs)]\nstatic mut CURRENTLY_PRESSED: Vec<String> = Vec::new();\n\n// Global state for tracking modifier keys to detect Cmd+C/Ctrl+C combinations\n#[allow(static_mut_refs)]\nstatic mut CMD_PRESSED: bool = false;\n#[allow(static_mut_refs)]\nstatic mut CTRL_PRESSED: bool = false;\n#[allow(static_mut_refs)]\nstatic mut COPY_IN_PROGRESS: bool = false;\n\n/// Prevents macOS App Nap from suspending this process.\n/// Returns an activity token that must be retained for the entire process\n/// lifetime. On non-macOS platforms, returns a dummy value.\n#[cfg(target_os = \"macos\")]\nfn prevent_app_nap() -> id {\n    unsafe {\n        let process_info = NSProcessInfo::processInfo(nil);\n        let reason = NSString::alloc(nil)\n            .init_str(\"Keyboard event monitoring requires continuous operation\");\n\n        // NSActivityOptions flags:\n        // NSActivityUserInitiated = 0x00FFFFFF (includes all protective flags)\n        // This prevents App Nap and idle system sleep\n        let options: u64 = 0x00FFFFFF;\n\n        let activity: id = msg_send![process_info, beginActivityWithOptions:options reason:reason];\n\n        eprintln!(\"macOS App Nap prevention enabled for keyboard listener process\");\n        activity\n    }\n}\n\n#[cfg(not(target_os = \"macos\"))]\nfn prevent_app_nap() {\n    // No-op on non-macOS platforms\n}\n\nfn main() {\n    // Prevent macOS App Nap from suspending this process\n    // Must retain this for the entire process lifetime\n    #[allow(clippy::let_unit_value)]\n    let _activity = prevent_app_nap();\n    // Spawn a thread to read commands from stdin\n    thread::spawn(|| {\n        let stdin = io::stdin();\n        for line in stdin.lock().lines().map_while(Result::ok) {\n            match serde_json::from_str::<Command>(&line) {\n                Ok(command) => handle_command(command),\n                Err(e) => eprintln!(\"Error parsing command: {}\", e),\n            }\n        }\n    });\n\n    // Spawn heartbeat thread\n    thread::spawn(|| {\n        let mut heartbeat_id = 0u64;\n        loop {\n            thread::sleep(Duration::from_secs(10)); // Send heartbeat every 10 seconds\n\n            heartbeat_id += 1;\n            let heartbeat_json = json!({\n                \"type\": \"heartbeat_ping\",\n                \"id\": heartbeat_id.to_string(),\n                \"timestamp\": Utc::now().to_rfc3339()\n            });\n\n            println!(\"{}\", heartbeat_json);\n            io::stdout().flush().unwrap();\n        }\n    });\n\n    // Start grabbing events\n    if let Err(error) = grab(callback) {\n        eprintln!(\"Error: {:?}\", error);\n    }\n}\n\nfn handle_command(command: Command) {\n    match command {\n        Command::RegisterHotkeys { hotkeys } => unsafe {\n            REGISTERED_HOTKEYS = hotkeys.clone();\n            eprintln!(\"Registered {} hotkeys\", REGISTERED_HOTKEYS.len());\n        },\n    }\n    io::stdout().flush().unwrap();\n}\n\n// Check if current pressed keys match any registered hotkey\nfn should_block() -> bool {\n    unsafe {\n        // Check each registered hotkey\n        for hotkey in &REGISTERED_HOTKEYS {\n            // A hotkey blocks when ALL its keys are currently pressed\n            let all_pressed = hotkey\n                .keys\n                .iter()\n                .all(|key| CURRENTLY_PRESSED.contains(key));\n\n            let same_length = hotkey.keys.len() == CURRENTLY_PRESSED.len();\n\n            if all_pressed && !hotkey.keys.is_empty() && same_length {\n                return true;\n            }\n        }\n        false\n    }\n}\n\nfn callback(event: Event) -> Option<Event> {\n    match event.event_type {\n        EventType::KeyPress(key) => {\n            let key_name = format!(\"{:?}\", key);\n\n            // Check for copy combinations before updating modifier states\n            // Ignore Cmd+C (macOS) and Ctrl+C (Windows/Linux) combinations to prevent\n            // feedback loops with selected-text-reader\n            if matches!(key, Key::KeyC) && unsafe { CMD_PRESSED || CTRL_PRESSED } {\n                unsafe {\n                    COPY_IN_PROGRESS = true;\n                }\n                // Still pass through the event to the system but don't output it to our\n                // listener\n                return Some(event);\n            }\n\n            // Update pressed keys BEFORE checking if we should block\n            // Normalize Unknown(179) to Function for detection purposes\n            let normalized_key = if key_name == \"Unknown(179)\" {\n                \"Function\".to_string()\n            } else {\n                key_name.clone()\n            };\n\n            unsafe {\n                if !CURRENTLY_PRESSED.contains(&normalized_key) {\n                    CURRENTLY_PRESSED.push(normalized_key);\n                }\n            }\n\n            // Track modifier key states\n            if matches!(key, Key::MetaLeft | Key::MetaRight) {\n                unsafe {\n                    CMD_PRESSED = true;\n                }\n            }\n            if matches!(key, Key::ControlLeft | Key::ControlRight) {\n                unsafe {\n                    CTRL_PRESSED = true;\n                }\n            }\n\n            output_event(\"keydown\", &key);\n\n            // Check if we should block based on exact hotkey match\n            #[allow(clippy::if_same_then_else)]\n            if should_block() {\n                // Windows-specific: Prevent Start menu from opening when Windows key is used in\n                // hotkeys Windows shows the Start menu if it sees \"Win down →\n                // Win up\" with no other keys in between. By injecting a\n                // harmless key (VK 0xFF), we \"poison\" the sequence so Windows thinks\n                // it was a combo, not a standalone Windows key press\n                #[cfg(target_os = \"windows\")]\n                unsafe {\n                    if CMD_PRESSED\n                        || CURRENTLY_PRESSED\n                            .iter()\n                            .any(|k| k == \"MetaLeft\" || k == \"MetaRight\")\n                    {\n                        // VK 0xFF is documented as \"no mapping\" - a valid key code with no function\n                        let _ = simulate(&EventType::KeyPress(Key::Unknown(0xFF)));\n                        let _ = simulate(&EventType::KeyRelease(Key::Unknown(0xFF)));\n                    }\n                }\n                None // Block the event from reaching the OS\n            } else if key_name == \"Unknown(179)\"\n                && unsafe {\n                    REGISTERED_HOTKEYS\n                        .iter()\n                        .any(|hotkey| hotkey.keys.contains(&\"Function\".to_string()))\n                }\n            {\n                None // Block Unknown(179) if any hotkey uses Function\n            } else {\n                Some(event) // Let it through\n            }\n        }\n        EventType::KeyRelease(key) => {\n            let key_name = format!(\"{:?}\", key);\n\n            // Normalize Unknown(179) to Function for detection purposes\n            let normalized_key = if key_name == \"Unknown(179)\" {\n                \"Function\".to_string()\n            } else {\n                key_name.clone()\n            };\n\n            // Update pressed keys\n            unsafe {\n                CURRENTLY_PRESSED.retain(|k| k != &normalized_key);\n            }\n\n            // Check for C key release while copy is in progress or modifiers are still held\n            if matches!(key, Key::KeyC)\n                && unsafe { COPY_IN_PROGRESS || CMD_PRESSED || CTRL_PRESSED }\n            {\n                unsafe {\n                    COPY_IN_PROGRESS = false;\n                }\n                // Don't output this C key release event\n                return Some(event);\n            }\n\n            // Track modifier key states\n            if matches!(key, Key::MetaLeft | Key::MetaRight) {\n                unsafe {\n                    CMD_PRESSED = false;\n                }\n            }\n            if matches!(key, Key::ControlLeft | Key::ControlRight) {\n                unsafe {\n                    CTRL_PRESSED = false;\n                }\n            }\n\n            output_event(\"keyup\", &key);\n\n            // Always allow key release events through\n            Some(event)\n        }\n        _ => Some(event), // Allow all other events\n    }\n}\n\nfn output_event(event_type: &str, key: &Key) {\n    let timestamp = Utc::now().to_rfc3339();\n    let key_name = format!(\"{:?}\", key);\n\n    let event_json = json!({\n        \"type\": event_type,\n        \"key\": key_name,\n        \"timestamp\": timestamp,\n        \"raw_code\": key_codes::key_to_code(key)\n    });\n\n    println!(\"{}\", event_json);\n    io::stdout().flush().unwrap();\n}\n"
  },
  {
    "path": "native/macos-text/.gitignore",
    "content": ".DS_Store\n/.build\n/Packages\nxcuserdata/\nDerivedData/\n.swiftpm/configuration/registries.json\n.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata\n.netrc\n"
  },
  {
    "path": "native/macos-text/Package.swift",
    "content": "// swift-tools-version:5.7\nimport PackageDescription\n\nlet package = Package(\n    name: \"macos-text\",\n    platforms: [.macOS(.v11)],\n    dependencies: [],\n    targets: [\n        .executableTarget(\n            name: \"focused-text-reader\",\n            dependencies: [],\n            linkerSettings: [\n                .linkedFramework(\"ApplicationServices\"),\n                .linkedFramework(\"Foundation\"),\n            ]\n        )\n    ]\n)\n"
  },
  {
    "path": "native/macos-text/Sources/focused-text-reader/main.swift",
    "content": "import AppKit\nimport Foundation\n\nfunc getFocusedText() -> String? {\n    // 1. Get the frontmost application.\n    guard let frontmostApp = NSWorkspace.shared.frontmostApplication else { return nil }\n    let pid = frontmostApp.processIdentifier\n    let appElement = AXUIElementCreateApplication(pid)\n\n    // 2. Get the focused element from that specific application.\n    var focusedElement: AnyObject?\n    let result = AXUIElementCopyAttributeValue(\n        appElement, kAXFocusedUIElementAttribute as CFString, &focusedElement)\n\n    if result != .success {\n        // If this fails, the app may not be cooperative.\n        return nil\n    }\n\n    // 3. From the focused element, get its value.\n    var textValue: AnyObject?\n    guard let element = focusedElement as! AXUIElement? else { return nil }\n    let valueResult = AXUIElementCopyAttributeValue(\n        element, kAXValueAttribute as CFString, &textValue)\n\n    if valueResult == .success {\n        return textValue as? String\n    }\n\n    return nil\n}\n\nlet output = getFocusedText() ?? \"\"\nprint(output)\n"
  },
  {
    "path": "native/rustfmt.toml",
    "content": "edition = \"2021\"\nmax_width = 100\ntab_spaces = 4\nnewline_style = \"Unix\"\nuse_small_heuristics = \"Default\"\nreorder_imports = true\nreorder_modules = true\nremove_nested_parens = true\nformat_code_in_doc_comments = true\nnormalize_comments = true\nwrap_comments = true\n"
  },
  {
    "path": "native/selected-text-reader/Cargo.toml",
    "content": "[package]\nname = \"selected-text-reader\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nserde = { version = \"1.0\", features = [\"derive\"] }\nserde_json = \"1.0\"\ncrossbeam-channel = \"0.5\"\narboard = \"3.4\"\nlru = \"0.12\"\nparking_lot = \"0.12\"\nenigo = \"0.6\"\n\n[target.'cfg(target_os = \"windows\")'.dependencies]\nselection = \"1.2.0\"\n\n[target.'cfg(target_os = \"macos\")'.dependencies]\naccessibility-ng = \"0.1.6\"\naccessibility-sys-ng = \"0.1\"\nactive-win-pos-rs = \"0.8\"\ncore-foundation = \"0.9\"\nlibc = \"0.2\"\n\n[build-dependencies]\ntauri-winres = \"0.3.5\"\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "native/selected-text-reader/build.rs",
    "content": "fn main() {\n    #[cfg(target_os = \"windows\")]\n    {\n        let mut res = tauri_winres::WindowsResource::new();\n\n        // Set all metadata\n        res.set_manifest_file(\"selected-text-reader.manifest\");\n        res.set(\"FileDescription\", \"Selected Text Reader - Captures selected text for accessibility and productivity applications\");\n        res.set(\"ProductName\", \"Selected Text Reader - Accessibility Tool\");\n        res.set(\"CompanyName\", \"Demox Labs\");\n        res.set(\n            \"LegalCopyright\",\n            \"Copyright © 2025 Demox Labs. All rights reserved.\",\n        );\n        res.set(\"FileVersion\", \"0.1.0.0\");\n        res.set(\"ProductVersion\", \"0.1.0.0\");\n        res.set(\"InternalName\", \"selected-text-reader\");\n        res.set(\"OriginalFilename\", \"selected-text-reader.exe\");\n        res.set(\n            \"Comments\",\n            \"Accessibility utility for selected text capture via command-line interface\",\n        );\n\n        res.compile().unwrap();\n    }\n}\n"
  },
  {
    "path": "native/selected-text-reader/selected-text-reader.manifest",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\">\n  <assemblyIdentity\n    version=\"0.1.0.0\"\n    processorArchitecture=\"*\"\n    name=\"DemoxLabs.SelectedTextReader.AccessibilityTool\"\n    type=\"win32\"\n  />\n  <description>Accessibility selected text capture utility for enhanced productivity</description>\n\n  <!-- Execution Level -->\n  <trustInfo xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <security>\n      <requestedPrivileges>\n        <requestedExecutionLevel level=\"asInvoker\" uiAccess=\"false\" />\n      </requestedPrivileges>\n    </security>\n  </trustInfo>\n\n  <!-- Windows 10/11 Compatibility -->\n  <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n    <application>\n      <!-- Windows 10+ -->\n      <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\"/>\n    </application>\n  </compatibility>\n\n  <!-- DPI Awareness -->\n  <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <windowsSettings>\n      <dpiAware xmlns=\"http://schemas.microsoft.com/SMI/2005/WindowsSettings\">true</dpiAware>\n      <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2</dpiAwareness>\n    </windowsSettings>\n  </application>\n</assembly>\n"
  },
  {
    "path": "native/selected-text-reader/src/macos.rs",
    "content": "use arboard::Clipboard;\nuse libc::c_void;\nuse std::ptr;\nuse std::thread;\nuse std::time::Duration;\n\n// Count characters as the editor sees them (on macOS, just use normal char\n// count)\npub fn count_editor_chars(text: &str) -> usize {\n    text.chars().count()\n}\n\n// Raw Quartz C API bindings for CGEventCreateKeyboardEvent\n#[repr(C)]\nstruct __CGEvent(c_void);\ntype CGEventRef = *mut __CGEvent;\n\ntype CGKeyCode = u16;\ntype CGEventFlags = u64;\nconst CG_EVENT_FLAG_MASK_COMMAND: CGEventFlags = 0x100000;\nconst CG_EVENT_FLAG_MASK_SHIFT: CGEventFlags = 0x020000;\n\ntype CGEventTapLocation = u32;\nconst CG_SESSION_EVENT_TAP: CGEventTapLocation = 1;\n\nextern \"C\" {\n    fn CGEventCreateKeyboardEvent(\n        source: *mut c_void,\n        virtualKey: CGKeyCode,\n        keyDown: bool,\n    ) -> CGEventRef;\n    fn CGEventSetFlags(event: CGEventRef, flags: CGEventFlags);\n    fn CGEventPost(tap: CGEventTapLocation, event: CGEventRef);\n    fn CGEventSetIntegerValueField(event: CGEventRef, field: u32, value: i64);\n    fn CFRelease(cf: *const c_void);\n}\n\npub fn get_selected_text() -> Result<String, Box<dyn std::error::Error>> {\n    // Simple approach: use Cmd+C (copy) to get any selected text\n    let mut clipboard = Clipboard::new().map_err(|e| format!(\"Clipboard init failed: {}\", e))?;\n\n    // Store original clipboard contents\n    let original_clipboard = clipboard.get_text().unwrap_or_default();\n\n    clipboard\n        .clear()\n        .map_err(|e| format!(\"Clipboard clear failed: {}\", e))?;\n\n    // Use Cmd+C to cut any selected text\n    native_cmd_c()?;\n\n    // Small delay for copy operation to complete\n    thread::sleep(Duration::from_millis(25));\n\n    // Get the copied text from clipboard (this is what was selected)\n    let selected_text = clipboard.get_text().unwrap_or_default();\n\n    // Always restore original clipboard contents - ITO is cutting on behalf of user\n    // for context\n    let _ = clipboard.set_text(original_clipboard);\n\n    Ok(selected_text)\n}\n\n// Native macOS Cmd+C implementation using raw Quartz C API - matching Python\n// exactly\npub fn native_cmd_c() -> Result<(), Box<dyn std::error::Error>> {\n    unsafe {\n        // Key code for 'C' is 8 on macOS\n        let c_key_code: CGKeyCode = 8;\n\n        // Create key down event for Cmd+C - using None as source like Python\n        let key_down_event = CGEventCreateKeyboardEvent(ptr::null_mut(), c_key_code, true);\n        if key_down_event.is_null() {\n            return Err(\"Failed to create key down event\".into());\n        }\n\n        // Set Command flag\n        CGEventSetFlags(key_down_event, CG_EVENT_FLAG_MASK_COMMAND);\n\n        // Create key up event for Cmd+C - using None as source like Python\n        let key_up_event = CGEventCreateKeyboardEvent(ptr::null_mut(), c_key_code, false);\n        if key_up_event.is_null() {\n            CFRelease(key_down_event as *const c_void);\n            return Err(\"Failed to create key up event\".into());\n        }\n\n        // Set Command flag\n        CGEventSetFlags(key_up_event, CG_EVENT_FLAG_MASK_COMMAND);\n\n        // Post the events with timing like Python\n        CGEventPost(CG_SESSION_EVENT_TAP, key_down_event);\n\n        // Small delay between down and up like Python does\n        thread::sleep(Duration::from_millis(10));\n\n        CGEventPost(CG_SESSION_EVENT_TAP, key_up_event);\n\n        // Clean up\n        CFRelease(key_down_event as *const c_void);\n        CFRelease(key_up_event as *const c_void);\n    }\n\n    Ok(())\n}\n\n// Simple function to select previous N characters and copy them\npub fn select_previous_chars_and_copy(\n    char_count: usize,\n    clipboard: &mut Clipboard,\n) -> Result<String, Box<dyn std::error::Error>> {\n    // Send Shift+Left N times to select precursor text (copied from working\n    // get_context)\n    for _i in 0..char_count {\n        unsafe {\n            let key_down_event = CGEventCreateKeyboardEvent(ptr::null_mut(), 123, true); // Left arrow\n            let key_up_event = CGEventCreateKeyboardEvent(ptr::null_mut(), 123, false);\n\n            if key_down_event.is_null() || key_up_event.is_null() {\n                if !key_down_event.is_null() {\n                    CFRelease(key_down_event as *const c_void);\n                }\n                if !key_up_event.is_null() {\n                    CFRelease(key_up_event as *const c_void);\n                }\n                return Err(\"Failed to create shift+left event\".into());\n            }\n\n            // Set Shift flag for selection\n            CGEventSetFlags(key_down_event, CG_EVENT_FLAG_MASK_SHIFT);\n            CGEventSetFlags(key_up_event, CG_EVENT_FLAG_MASK_SHIFT);\n\n            // Mark as synthetic events\n            CGEventSetIntegerValueField(key_down_event, 121, 0x49544F);\n            CGEventSetIntegerValueField(key_up_event, 121, 0x49544F);\n\n            // Post events using session event tap to avoid interference\n            CGEventPost(CG_SESSION_EVENT_TAP, key_down_event);\n            thread::sleep(Duration::from_millis(2));\n            CGEventPost(CG_SESSION_EVENT_TAP, key_up_event);\n\n            CFRelease(key_down_event as *const c_void);\n            CFRelease(key_up_event as *const c_void);\n        }\n\n        // Brief pause between selections\n        thread::sleep(Duration::from_millis(1));\n    }\n\n    // Allow selection to complete (match working get_context timing)\n    thread::sleep(Duration::from_millis(10));\n\n    native_cmd_c()?;\n\n    // Adaptively wait for and get text from clipboard\n    let mut context_text = String::new();\n    let max_retries = 20; // Poll for a maximum of 20 * 10ms = 200ms\n    for _ in 0..max_retries {\n        // Give a tiny bit of time for the clipboard to update\n        thread::sleep(Duration::from_millis(10));\n\n        if let Ok(text) = clipboard.get_text() {\n            if !text.is_empty() {\n                context_text = text;\n                break; // Success! We got the text.\n            }\n        }\n    }\n\n    Ok(context_text)\n}\n\n// Shift cursor right while deselecting text\npub fn shift_cursor_right_with_deselect(\n    char_count: usize,\n) -> Result<(), Box<dyn std::error::Error>> {\n    if char_count == 0 {\n        return Ok(());\n    }\n\n    for _i in 0..char_count {\n        unsafe {\n            let right_arrow_key_code: CGKeyCode = 124; // Right Arrow key code\n            let key_down = CGEventCreateKeyboardEvent(ptr::null_mut(), right_arrow_key_code, true);\n            let key_up = CGEventCreateKeyboardEvent(ptr::null_mut(), right_arrow_key_code, false);\n\n            if !key_down.is_null() && !key_up.is_null() {\n                // Set Shift flag to unselect the text as we move right\n                CGEventSetFlags(key_down, CG_EVENT_FLAG_MASK_SHIFT);\n                CGEventSetFlags(key_up, CG_EVENT_FLAG_MASK_SHIFT);\n\n                // Mark as synthetic events\n                CGEventSetIntegerValueField(key_down, 121, 0x49544F);\n                CGEventSetIntegerValueField(key_up, 121, 0x49544F);\n\n                CGEventPost(CG_SESSION_EVENT_TAP, key_down);\n                CGEventPost(CG_SESSION_EVENT_TAP, key_up);\n\n                CFRelease(key_down as *const c_void);\n                CFRelease(key_up as *const c_void);\n            }\n        }\n\n        // Brief pause between movements\n        if char_count > 1 {\n            thread::sleep(Duration::from_millis(1));\n        }\n    }\n\n    Ok(())\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_count_editor_chars() {\n        assert_eq!(count_editor_chars(\"hello\"), 5);\n        assert_eq!(count_editor_chars(\"Hello 世界\"), 8);\n        assert_eq!(count_editor_chars(\"Hi 👋\"), 4);\n        assert_eq!(count_editor_chars(\"\"), 0);\n    }\n}\n"
  },
  {
    "path": "native/selected-text-reader/src/main.rs",
    "content": "use arboard::Clipboard;\nuse serde::{Deserialize, Serialize};\nuse std::io::{self, BufRead, Write};\nuse std::thread;\nuse std::time::Duration;\n\n// Platform-specific modules\n#[cfg(target_os = \"macos\")]\nmod macos;\n#[cfg(target_os = \"windows\")]\nmod windows;\n\n#[derive(Serialize, Deserialize, Debug, Clone)]\n#[serde(tag = \"command\")]\nenum Command {\n    #[serde(rename = \"get-text\")]\n    GetText {\n        format: Option<String>,\n        #[serde(rename = \"maxLength\")]\n        max_length: Option<usize>,\n        #[serde(rename = \"requestId\")]\n        request_id: String,\n    },\n    #[serde(rename = \"get-cursor-context\")]\n    GetCursorContext {\n        #[serde(rename = \"contextLength\")]\n        context_length: Option<usize>,\n        #[serde(rename = \"cutCurrentSelection\")]\n        cut_current_selection: Option<bool>,\n        #[serde(rename = \"requestId\")]\n        request_id: String,\n    },\n}\n\n#[derive(Serialize)]\nstruct SelectedTextResponse {\n    #[serde(rename = \"requestId\")]\n    request_id: String,\n    success: bool,\n    text: Option<String>,\n    error: Option<String>,\n    length: usize,\n}\n\n#[derive(Serialize)]\nstruct CursorContextResponse {\n    #[serde(rename = \"requestId\")]\n    request_id: String,\n    success: bool,\n    #[serde(rename = \"contextText\")]\n    context_text: Option<String>,\n    error: Option<String>,\n    length: usize,\n}\n\nfn main() {\n    let (cmd_tx, cmd_rx) = crossbeam_channel::unbounded::<Command>();\n\n    let mut command_processor = CommandProcessor::new(cmd_rx);\n\n    // Spawn thread to read commands from stdin\n    thread::spawn(move || {\n        let stdin = io::stdin();\n        for l in stdin.lock().lines().map_while(Result::ok) {\n            if l.trim().is_empty() {\n                continue;\n            }\n            if let Ok(command) = serde_json::from_str::<Command>(&l) {\n                if let Err(e) = cmd_tx.send(command) {\n                    eprintln!(\n                        \"[selected-text-reader] Failed to send command to processor: {}\",\n                        e\n                    );\n                    break;\n                }\n            }\n        }\n    });\n\n    command_processor.run();\n}\n\nstruct CommandProcessor {\n    cmd_rx: crossbeam_channel::Receiver<Command>,\n}\n\nimpl CommandProcessor {\n    fn new(cmd_rx: crossbeam_channel::Receiver<Command>) -> Self {\n        CommandProcessor { cmd_rx }\n    }\n\n    fn run(&mut self) {\n        while let Ok(command) = self.cmd_rx.recv() {\n            match command {\n                Command::GetText {\n                    format: _,\n                    max_length,\n                    request_id,\n                } => self.handle_get_text(max_length, request_id),\n                Command::GetCursorContext {\n                    context_length,\n                    cut_current_selection,\n                    request_id,\n                } => self.handle_get_cursor_context(\n                    context_length,\n                    cut_current_selection,\n                    request_id,\n                ),\n            }\n        }\n    }\n\n    fn handle_get_text(&mut self, max_length: Option<usize>, request_id: String) {\n        let max_len = max_length.unwrap_or(10000);\n\n        let response = match get_selected_text() {\n            Ok(selected_text) => {\n                let text = if selected_text.is_empty() {\n                    None\n                } else if selected_text.len() > max_len {\n                    Some(selected_text.chars().take(max_len).collect())\n                } else {\n                    Some(selected_text)\n                };\n\n                SelectedTextResponse {\n                    request_id,\n                    success: true,\n                    text: text.clone(),\n                    error: None,\n                    length: text.as_ref().map(|t| t.len()).unwrap_or(0),\n                }\n            }\n            Err(e) => SelectedTextResponse {\n                request_id,\n                success: false,\n                text: None,\n                error: Some(format!(\"Failed to get selected text: {}\", e)),\n                length: 0,\n            },\n        };\n\n        // Always respond with JSON\n        match serde_json::to_string(&response) {\n            Ok(json) => {\n                println!(\"{}\", json);\n                if let Err(e) = io::stdout().flush() {\n                    eprintln!(\"[selected-text-reader] Error flushing stdout: {}\", e);\n                }\n            }\n            Err(e) => {\n                eprintln!(\n                    \"[selected-text-reader] Error serializing response to JSON: {}\",\n                    e\n                );\n            }\n        }\n    }\n\n    fn handle_get_cursor_context(\n        &mut self,\n        context_length: Option<usize>,\n        _cut_current_selection: Option<bool>,\n        request_id: String,\n    ) {\n        let context_len = context_length.unwrap_or(10);\n\n        let response = match get_cursor_context(context_len) {\n            Ok(context_text) => {\n                let text = if context_text.is_empty() {\n                    None\n                } else {\n                    Some(context_text.clone())\n                };\n\n                CursorContextResponse {\n                    request_id,\n                    success: true,\n                    context_text: text.clone(),\n                    error: None,\n                    length: text.as_ref().map(|t| t.len()).unwrap_or(0),\n                }\n            }\n            Err(e) => CursorContextResponse {\n                request_id,\n                success: false,\n                context_text: None,\n                error: Some(format!(\"Failed to get cursor context: {}\", e)),\n                length: 0,\n            },\n        };\n\n        // Always respond with JSON\n        match serde_json::to_string(&response) {\n            Ok(json) => {\n                println!(\"{}\", json);\n                if let Err(e) = io::stdout().flush() {\n                    eprintln!(\"[selected-text-reader] Error flushing stdout: {}\", e);\n                }\n            }\n            Err(e) => {\n                eprintln!(\n                    \"[selected-text-reader] Error serializing response to JSON: {}\",\n                    e\n                );\n            }\n        }\n    }\n}\n\n// Platform-specific implementations\n#[cfg(target_os = \"macos\")]\nfn get_selected_text() -> Result<String, Box<dyn std::error::Error>> {\n    macos::get_selected_text()\n}\n\n#[cfg(target_os = \"windows\")]\nfn get_selected_text() -> Result<String, Box<dyn std::error::Error>> {\n    windows::get_selected_text()\n}\n\nfn get_cursor_context(context_length: usize) -> Result<String, Box<dyn std::error::Error>> {\n    // Use keyboard commands to get cursor context\n    // This is more reliable across different applications than Accessibility API\n    let mut clipboard = Clipboard::new().map_err(|e| format!(\"Clipboard init failed: {}\", e))?;\n\n    // Store original clipboard contents\n    let original_clipboard = clipboard.get_text().unwrap_or_default();\n\n    // First, get any existing selected text\n    clipboard\n        .clear()\n        .map_err(|e| format!(\"Clipboard clear failed: {}\", e))?;\n    copy_selected_text()?;\n    thread::sleep(Duration::from_millis(25));\n    let selected_text = clipboard.get_text().unwrap_or_default();\n    let selected_char_count = count_editor_chars(&selected_text);\n\n    let context_text = if selected_char_count == 0 {\n        // Case 1: No selected text - proceed normally with cursor context\n        clipboard\n            .clear()\n            .map_err(|e| format!(\"Clipboard clear failed: {}\", e))?;\n\n        let result = select_previous_chars_and_copy(context_length, &mut clipboard);\n        match result {\n            Ok(precursor_text) => {\n                let precursor_char_count = count_editor_chars(&precursor_text);\n                // Shift right by the amount we grabbed\n                if precursor_char_count > 0 {\n                    let _ = shift_cursor_right_with_deselect(precursor_char_count);\n                }\n                precursor_text\n            }\n            Err(e) => format!(\"[ERROR] {}\", e),\n        }\n    } else {\n        // Case 2: Some text already selected - try extending by one character\n        clipboard\n            .clear()\n            .map_err(|e| format!(\"Clipboard clear failed: {}\", e))?;\n\n        let result = select_previous_chars_and_copy(1, &mut clipboard);\n        match result {\n            Ok(extended_text) => {\n                let extended_char_count = count_editor_chars(&extended_text);\n\n                if extended_char_count < selected_char_count {\n                    // Selection shrunk - undo and return empty\n                    let _ = shift_cursor_right_with_deselect(1);\n                    String::new()\n                } else if extended_char_count == selected_char_count {\n                    // Selection unchanged - return empty, no need to return cursor.\n                    // Likely means we're at the edge of a textbox.\n                    String::new()\n                } else {\n                    // Selection extended successfully - continue extending to get full\n                    // context_length\n                    clipboard\n                        .clear()\n                        .map_err(|e| format!(\"Clipboard clear failed: {}\", e))?;\n\n                    let full_result =\n                        select_previous_chars_and_copy(context_length - 1, &mut clipboard);\n                    match full_result {\n                        Ok(full_context_text) => {\n                            let full_context_char_count = count_editor_chars(&full_context_text);\n                            // Undo by the absolute difference between original selected text and\n                            // total selection\n                            let chars_to_undo =\n                                (full_context_char_count as i32 - selected_char_count as i32)\n                                    .unsigned_abs() as usize;\n                            if chars_to_undo > 0 {\n                                let _ = shift_cursor_right_with_deselect(chars_to_undo);\n                            }\n\n                            // Return only the newly added context (first n characters where n is\n                            // the difference)\n                            let new_context_char_count =\n                                full_context_char_count - selected_char_count;\n                            full_context_text\n                                .chars()\n                                .take(new_context_char_count)\n                                .collect()\n                        }\n                        Err(e) => format!(\"[ERROR] {}\", e),\n                    }\n                }\n            }\n            Err(e) => format!(\"[ERROR] {}\", e),\n        }\n    };\n\n    // Always restore original clipboard\n    let _ = clipboard.set_text(original_clipboard);\n\n    Ok(context_text)\n}\n\n// Platform-specific helper functions\n#[cfg(target_os = \"macos\")]\nfn copy_selected_text() -> Result<(), Box<dyn std::error::Error>> {\n    macos::native_cmd_c()\n}\n\n#[cfg(target_os = \"windows\")]\nfn copy_selected_text() -> Result<(), Box<dyn std::error::Error>> {\n    windows::copy_selected_text()\n}\n\n#[cfg(target_os = \"macos\")]\nfn select_previous_chars_and_copy(\n    char_count: usize,\n    clipboard: &mut Clipboard,\n) -> Result<String, Box<dyn std::error::Error>> {\n    macos::select_previous_chars_and_copy(char_count, clipboard)\n}\n\n#[cfg(target_os = \"windows\")]\nfn select_previous_chars_and_copy(\n    char_count: usize,\n    clipboard: &mut Clipboard,\n) -> Result<String, Box<dyn std::error::Error>> {\n    windows::select_previous_chars_and_copy(char_count, clipboard)\n}\n\n#[cfg(target_os = \"macos\")]\nfn shift_cursor_right_with_deselect(char_count: usize) -> Result<(), Box<dyn std::error::Error>> {\n    macos::shift_cursor_right_with_deselect(char_count)\n}\n\n#[cfg(target_os = \"windows\")]\nfn shift_cursor_right_with_deselect(char_count: usize) -> Result<(), Box<dyn std::error::Error>> {\n    windows::shift_cursor_right_with_deselect(char_count)\n}\n\n#[cfg(target_os = \"macos\")]\nfn count_editor_chars(text: &str) -> usize {\n    macos::count_editor_chars(text)\n}\n\n#[cfg(target_os = \"windows\")]\nfn count_editor_chars(text: &str) -> usize {\n    windows::count_editor_chars(text)\n}\n"
  },
  {
    "path": "native/selected-text-reader/src/windows.rs",
    "content": "use arboard::Clipboard;\nuse selection::get_text;\nuse std::thread;\nuse std::time::Duration;\n\n// Count characters as the editor sees them (CRLF = 1 cursor position on\n// Windows)\npub fn count_editor_chars(text: &str) -> usize {\n    // On Windows, editors treat CRLF as a single cursor position when navigating\n    // with arrow keys\n    text.replace(\"\\r\\n\", \"\\n\").chars().count()\n}\n\npub fn get_selected_text() -> Result<String, Box<dyn std::error::Error>> {\n    let selected_text = get_text();\n    Ok(selected_text)\n}\n\npub fn copy_selected_text() -> Result<(), Box<dyn std::error::Error>> {\n    use enigo::{Direction, Enigo, Key, Keyboard, Settings};\n\n    let mut enigo = Enigo::new(&Settings::default())?;\n    enigo.key(Key::Control, Direction::Press)?;\n    enigo.key(Key::Unicode('c'), Direction::Click)?;\n    enigo.key(Key::Control, Direction::Release)?;\n\n    Ok(())\n}\n\n// Simple function to select previous N characters and copy them\npub fn select_previous_chars_and_copy(\n    char_count: usize,\n    clipboard: &mut Clipboard,\n) -> Result<String, Box<dyn std::error::Error>> {\n    // Send Shift+Left N times to select precursor text\n    for _ in 0..char_count {\n        #[cfg(target_os = \"windows\")]\n        {\n            use enigo::{Direction, Enigo, Key, Keyboard, Settings};\n            let mut enigo = Enigo::new(&Settings::default())?;\n            enigo.key(Key::Shift, Direction::Press)?;\n            enigo.key(Key::LeftArrow, Direction::Click)?;\n            enigo.key(Key::Shift, Direction::Release)?;\n        }\n\n        // Brief pause between selections\n        thread::sleep(Duration::from_millis(1));\n    }\n\n    // Allow selection to complete\n    thread::sleep(Duration::from_millis(10));\n\n    copy_selected_text()?;\n\n    // Adaptively wait for and get text from clipboard\n    let mut context_text = String::new();\n    let max_retries = 20; // Poll for a maximum of 20 * 10ms = 200ms\n    for _ in 0..max_retries {\n        // Give a tiny bit of time for the clipboard to update\n        thread::sleep(Duration::from_millis(10));\n\n        if let Ok(text) = clipboard.get_text() {\n            if !text.is_empty() {\n                context_text = text;\n                break; // Success! We got the text.\n            }\n        }\n    }\n\n    Ok(context_text)\n}\n\n// Shift cursor right while deselecting text\npub fn shift_cursor_right_with_deselect(\n    char_count: usize,\n) -> Result<(), Box<dyn std::error::Error>> {\n    if char_count == 0 {\n        return Ok(());\n    }\n\n    for _ in 0..char_count {\n        {\n            use enigo::{Direction, Enigo, Key, Keyboard, Settings};\n            let mut enigo = Enigo::new(&Settings::default())?;\n            enigo.key(Key::Shift, Direction::Press)?;\n            enigo.key(Key::RightArrow, Direction::Click)?;\n            enigo.key(Key::Shift, Direction::Release)?;\n        }\n        // Brief pause between movements\n        if char_count > 1 {\n            thread::sleep(Duration::from_millis(1));\n        }\n    }\n\n    Ok(())\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_count_editor_chars_normal_text() {\n        assert_eq!(count_editor_chars(\"hello\"), 5);\n    }\n\n    #[test]\n    fn test_count_editor_chars_with_unix_newline() {\n        assert_eq!(count_editor_chars(\"line1\\nline2\"), 11);\n    }\n\n    #[test]\n    fn test_count_editor_chars_with_crlf() {\n        // Windows CRLF should count as single character\n        assert_eq!(count_editor_chars(\"line1\\r\\nline2\"), 11);\n    }\n\n    #[test]\n    fn test_count_editor_chars_multiple_crlf() {\n        assert_eq!(count_editor_chars(\"a\\r\\nb\\r\\nc\"), 5);\n    }\n\n    #[test]\n    fn test_count_editor_chars_unicode() {\n        assert_eq!(count_editor_chars(\"Hello 世界\"), 8);\n    }\n\n    #[test]\n    fn test_count_editor_chars_emoji() {\n        assert_eq!(count_editor_chars(\"Hi 👋\"), 4);\n    }\n\n    #[test]\n    fn test_count_editor_chars_empty() {\n        assert_eq!(count_editor_chars(\"\"), 0);\n    }\n}\n"
  },
  {
    "path": "native/text-writer/Cargo.toml",
    "content": "[package]\nname = \"text-writer\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nclap = { version = \"4.5\", features = [\"derive\"] }\n\n[target.'cfg(target_os = \"linux\")'.dependencies]\nenigo = \"0.3.0\"\n\n[target.'cfg(target_os = \"windows\")'.dependencies]\nenigo = \"0.3.0\"\nclipboard-win = \"5.0\"\n\n[target.'cfg(target_os = \"macos\")'.dependencies]\ncore-graphics = \"0.23\"\ncore-foundation = \"0.9\"\ncocoa = \"0.25\"\n\n[build-dependencies]\ntauri-winres = \"0.3.5\"\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "native/text-writer/build.rs",
    "content": "fn main() {\n    #[cfg(target_os = \"windows\")]\n    {\n        let mut res = tauri_winres::WindowsResource::new();\n\n        // Set all metadata\n        res.set_manifest_file(\"text-writer.manifest\");\n        res.set(\"FileDescription\", \"Accessibility Text Input Utility - Assists with text entry for productivity applications\");\n        res.set(\"ProductName\", \"Text Writer - Accessibility Tool\");\n        res.set(\"CompanyName\", \"Demox Labs\");\n        res.set(\n            \"LegalCopyright\",\n            \"Copyright © 2025 Demox Labs. All rights reserved.\",\n        );\n        res.set(\"FileVersion\", \"0.1.0.0\");\n        res.set(\"ProductVersion\", \"0.1.0.0\");\n        res.set(\"InternalName\", \"text-writer\");\n        res.set(\"OriginalFilename\", \"text-writer.exe\");\n        res.set(\n            \"Comments\",\n            \"Accessibility utility for enhanced text input via command-line interface\",\n        );\n\n        res.compile().unwrap();\n    }\n}\n"
  },
  {
    "path": "native/text-writer/src/macos_writer.rs",
    "content": "#[cfg(target_os = \"macos\")]\nuse cocoa::appkit::{NSPasteboard, NSPasteboardTypeString};\nuse cocoa::base::nil;\nuse cocoa::foundation::{NSAutoreleasePool, NSString};\nuse core_graphics::event::{CGEvent, CGEventFlags};\nuse core_graphics::event_source::{CGEventSource, CGEventSourceStateID};\nuse std::thread;\nuse std::time::Duration;\n\n/// Type text on macOS using clipboard paste approach\n/// This avoids character-by-character typing which can cause issues in some\n/// apps\npub fn type_text_macos(text: &str, _char_delay: u64) -> Result<(), String> {\n    unsafe {\n        // Create an autorelease pool for memory management\n        let _pool = NSAutoreleasePool::new(nil);\n\n        // Get the general pasteboard\n        let pasteboard = NSPasteboard::generalPasteboard(nil);\n\n        // Store current clipboard contents to restore later\n        let old_contents = pasteboard.stringForType(NSPasteboardTypeString);\n\n        // Clear the pasteboard and set our text\n        pasteboard.clearContents();\n        let ns_string = NSString::alloc(nil).init_str(text);\n        pasteboard.setString_forType(ns_string, NSPasteboardTypeString);\n\n        // Verify clipboard was actually set by reading it back\n        let mut attempts = 0;\n        loop {\n            let current_content = pasteboard.stringForType(NSPasteboardTypeString);\n            if current_content != nil {\n                let current_str = cocoa::foundation::NSString::UTF8String(current_content);\n                let current_rust_str = std::ffi::CStr::from_ptr(current_str)\n                    .to_string_lossy()\n                    .into_owned();\n                if current_rust_str == text {\n                    break;\n                }\n            }\n\n            attempts += 1;\n            if attempts > 50 {\n                return Err(\"Failed to verify clipboard content was set\".to_string());\n            }\n            thread::sleep(Duration::from_millis(2));\n        }\n\n        // Create event source\n        let source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState)\n            .map_err(|_| \"Failed to create event source\")?;\n\n        // Simulate Cmd+V (paste)\n        // Key code 9 is 'V' key\n        let key_v_down = CGEvent::new_keyboard_event(source.clone(), 9, true)\n            .map_err(|_| \"Failed to create key down event\")?;\n        let key_v_up = CGEvent::new_keyboard_event(source.clone(), 9, false)\n            .map_err(|_| \"Failed to create key up event\")?;\n\n        // Set the Command modifier flag\n        key_v_down.set_flags(CGEventFlags::CGEventFlagCommand);\n        key_v_up.set_flags(CGEventFlags::CGEventFlagCommand);\n\n        // Post the events\n        key_v_down.post(core_graphics::event::CGEventTapLocation::HID);\n        thread::sleep(Duration::from_millis(10));\n        key_v_up.post(core_graphics::event::CGEventTapLocation::HID);\n\n        // Restore old clipboard contents in background after delay in separate thread\n        // to not block\n        if old_contents != nil {\n            // Convert Objective-C string to Rust String to make it Send-safe\n            let old_contents_str = {\n                let c_str = cocoa::foundation::NSString::UTF8String(old_contents);\n                std::ffi::CStr::from_ptr(c_str)\n                    .to_string_lossy()\n                    .into_owned()\n            };\n\n            thread::sleep(Duration::from_secs(1));\n            let pasteboard = NSPasteboard::generalPasteboard(nil);\n            pasteboard.clearContents();\n            let ns_string = NSString::alloc(nil).init_str(&old_contents_str);\n            pasteboard.setString_forType(ns_string, NSPasteboardTypeString);\n        }\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "native/text-writer/src/main.rs",
    "content": "use clap::Parser;\nuse std::process;\nuse std::thread;\nuse std::time::Duration;\n\n#[cfg(target_os = \"linux\")]\nuse enigo::{Enigo, Key, Keyboard, Settings};\n\n#[cfg(target_os = \"macos\")]\nmod macos_writer;\n#[cfg(target_os = \"macos\")]\nuse macos_writer::type_text_macos;\n\n#[cfg(target_os = \"windows\")]\nmod windows_writer;\n#[cfg(target_os = \"windows\")]\nuse windows_writer::type_text_windows;\n\n#[derive(Parser)]\n#[command(name = \"text-writer\")]\n#[command(about = \"A cross-platform text typing utility\")]\n#[command(version = \"0.1.0\")]\nstruct Args {\n    #[arg(help = \"Text to type\")]\n    text: String,\n\n    #[arg(\n        short,\n        long,\n        default_value_t = 0,\n        help = \"Delay before typing (milliseconds)\"\n    )]\n    delay: u64,\n\n    #[arg(\n        short,\n        long,\n        default_value_t = 0,\n        help = \"Delay between characters (milliseconds)\"\n    )]\n    char_delay: u64,\n}\n\nfn main() {\n    let args = Args::parse();\n\n    if args.text.is_empty() {\n        eprintln!(\"Error: Text cannot be empty\");\n        process::exit(1);\n    }\n\n    if args.delay > 0 {\n        thread::sleep(Duration::from_millis(args.delay));\n    }\n\n    // Use platform-specific implementation\n    #[cfg(target_os = \"macos\")]\n    {\n        if let Err(e) = type_text_macos(&args.text, args.char_delay) {\n            eprintln!(\"Error typing text: {}\", e);\n            process::exit(1);\n        }\n    }\n\n    #[cfg(target_os = \"windows\")]\n    {\n        if let Err(e) = type_text_windows(&args.text, args.char_delay) {\n            eprintln!(\"Error typing text: {}\", e);\n            process::exit(1);\n        }\n    }\n\n    #[cfg(target_os = \"linux\")]\n    {\n        let mut enigo = match Enigo::new(&Settings::default()) {\n            Ok(enigo) => enigo,\n            Err(e) => {\n                eprintln!(\"Error initializing enigo: {}\", e);\n                process::exit(1);\n            }\n        };\n\n        if args.char_delay > 0 {\n            for ch in args.text.chars() {\n                if let Err(e) = enigo.text(&ch.to_string()) {\n                    eprintln!(\"Error typing character '{}': {}\", ch, e);\n                    process::exit(1);\n                }\n                thread::sleep(Duration::from_millis(args.char_delay));\n            }\n        } else {\n            if let Err(e) = enigo.text(&args.text) {\n                eprintln!(\"Error typing text: {}\", e);\n                process::exit(1);\n            }\n        }\n\n        // Patch fix: Send 'A' key release to clean up any phantom stuck KeyA events\n        // This addresses a bug where synthetic events from text typing can cause\n        // the global key listener to receive keydown events without corresponding keyup\n        // events\n        if let Err(e) = enigo.key(Key::Unicode('a'), enigo::Direction::Release) {\n            // Don't exit on this error since it's just a cleanup operation\n            eprintln!(\"Warning: Failed to send cleanup 'a' key release: {}\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "native/text-writer/src/windows_writer.rs",
    "content": "#[cfg(target_os = \"windows\")]\nuse clipboard_win::{formats, get_clipboard, set_clipboard};\nuse enigo::{Enigo, Key, Keyboard, Settings};\nuse std::thread;\nuse std::time::Duration;\n\n/// Type text on Windows using clipboard paste approach\n/// This mimics the macOS implementation to avoid character-by-character typing\n/// issues\npub fn type_text_windows(text: &str, _char_delay: u64) -> Result<(), String> {\n    // Store current clipboard contents to restore later\n    let old_contents: Result<String, _> = get_clipboard(formats::Unicode);\n\n    // Set our text to clipboard\n    set_clipboard(formats::Unicode, text)\n        .map_err(|e| format!(\"Failed to set clipboard: {:?}\", e))?;\n\n    // Verify clipboard was actually set by reading it back\n    let mut attempts = 0;\n    loop {\n        match get_clipboard::<String, _>(formats::Unicode) {\n            Ok(content) if content == text => break,\n            _ => {\n                attempts += 1;\n                if attempts > 50 {\n                    return Err(\"Failed to verify clipboard content was set\".to_string());\n                }\n                thread::sleep(Duration::from_millis(2));\n            }\n        }\n    }\n\n    // Initialize enigo for keyboard simulation\n    let mut enigo = Enigo::new(&Settings::default())\n        .map_err(|e| format!(\"Failed to initialize enigo: {}\", e))?;\n\n    // Simulate Ctrl+V (paste)\n    // Press Ctrl\n    enigo\n        .key(Key::Control, enigo::Direction::Press)\n        .map_err(|e| format!(\"Failed to press Ctrl: {}\", e))?;\n\n    // Press V\n    enigo\n        .key(Key::Unicode('v'), enigo::Direction::Press)\n        .map_err(|e| format!(\"Failed to press V: {}\", e))?;\n\n    // Small delay to ensure the key press is registered\n    thread::sleep(Duration::from_millis(20));\n\n    // Release V\n    enigo\n        .key(Key::Unicode('v'), enigo::Direction::Release)\n        .map_err(|e| format!(\"Failed to release V: {}\", e))?;\n\n    // Release Ctrl\n    enigo\n        .key(Key::Control, enigo::Direction::Release)\n        .map_err(|e| format!(\"Failed to release Ctrl: {}\", e))?;\n\n    if let Ok(old_text) = old_contents {\n        thread::sleep(Duration::from_secs(1));\n        let _ = set_clipboard(formats::Unicode, &old_text);\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "native/text-writer/text-writer.manifest",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\">\n  <assemblyIdentity\n    version=\"0.1.0.0\"\n    processorArchitecture=\"*\"\n    name=\"DemoxLabs.TextWriter.AccessibilityTool\"\n    type=\"win32\"\n  />\n  <description>Accessibility text input utility for enhanced productivity</description>\n\n  <!-- Execution Level -->\n  <trustInfo xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <security>\n      <requestedPrivileges>\n        <requestedExecutionLevel level=\"asInvoker\" uiAccess=\"false\" />\n      </requestedPrivileges>\n    </security>\n  </trustInfo>\n\n  <!-- Windows 10/11 Compatibility -->\n  <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n    <application>\n      <!-- Windows 10+ -->\n      <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\"/>\n    </application>\n  </compatibility>\n\n  <!-- DPI Awareness -->\n  <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <windowsSettings>\n      <dpiAware xmlns=\"http://schemas.microsoft.com/SMI/2005/WindowsSettings\">true</dpiAware>\n      <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2</dpiAwareness>\n    </windowsSettings>\n  </application>\n</assembly>\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"ito\",\n  \"version\": \"0.2.3\",\n  \"description\": \"Ito, a smart voice assistant\",\n  \"main\": \"./out/main/main.js\",\n  \"license\": \"MIT\",\n  \"author\": {\n    \"name\": \"Demox Labs\",\n    \"url\": \"https://github.com/demox-labs\"\n  },\n  \"build\": {\n    \"extraResources\": [\n      {\n        \"from\": \"lib/generated\",\n        \"to\": \"proto\",\n        \"filter\": [\n          \"**/*.proto\"\n        ]\n      }\n    ]\n  },\n  \"scripts\": {\n    \"dev\": \"electron-vite dev -w\",\n    \"dev:rust\": \"bun build:rust:mac && bun dev\",\n    \"format:fix:app\": \"prettier --write app lib\",\n    \"format:fix\": \"prettier --write .\",\n    \"lint:fix:app\": \"eslint app lib --ext .ts,.tsx --fix\",\n    \"lint:fix\": \"eslint . --ext .ts,.tsx --fix\",\n    \"lint:app\": \"eslint app lib --ext .ts,.tsx\",\n    \"lint\": \"eslint . --ext .ts,.tsx\",\n    \"type-check\": \"tsc --noEmit --project tsconfig.node.json\",\n    \"format:app\": \"prettier app lib --check\",\n    \"format\": \"prettier . --check\",\n    \"runAllTests\": \"bun runLibTests && bun runServerTests && bun runAppTests && bun runNativeTests\",\n    \"runLibTests\": \"find lib -name \\\"*.test.ts\\\" | xargs -I {} sh -c 'bun test --preload lib/__tests__/setup.ts {} || exit 255'\",\n    \"runServerTests\": \"find server/src -name \\\"*.test.ts\\\" | xargs -I {} sh -c 'bun test {} || exit 255'\",\n    \"runAppTests\": \"find app -name \\\"*.test.ts\\\" | xargs -I {} sh -c 'bun test --preload lib/__tests__/setup.ts {} || exit 255'\",\n    \"runNativeTests\": \"cd native && cargo test --workspace --quiet\",\n    \"runTest\": \"bun test --preload lib/__tests__/setup.ts\",\n    \"format:native\": \"cd native && cargo fmt --all -- --check\",\n    \"format:fix:native\": \"cd native && cargo fmt --all\",\n    \"lint:native\": \"cd native && cargo clippy --workspace --all-targets -- -D warnings\",\n    \"lint:fix:native\": \"cd native && cargo clippy --workspace --all-targets --fix --allow-dirty --allow-staged\",\n    \"clean\": \"rm -rf node_modules dist .vite\",\n    \"clean:ito-app-data\": \"bun run scripts/clean-app-data.js\",\n    \"build:rust:mac\": \"bash ./build-binaries.sh --mac\",\n    \"build:rust:mac:x64\": \"bash ./build-binaries.sh --mac --x64\",\n    \"build:rust:win\": \"bash ./build-binaries.sh --windows\",\n    \"build:swift:mac\": \"cd native/cursor-context && swift build -c release\",\n    \"build:win\": \"bash ./build-app.sh windows\",\n    \"build:mac\": \"bash ./build-app.sh mac\",\n    \"generate:constants\": \"bun scripts/generate-constants.js\",\n    \"build:app\": \"bash ./build-app.sh\",\n    \"prepare\": \"husky\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/heyito/ito\"\n  },\n  \"pnpm\": {\n    \"onlyBuiltDependencies\": [\n      \"electron\",\n      \"esbuild\"\n    ]\n  },\n  \"dependencies\": {\n    \"@auth0/auth0-react\": \"^2.3.0\",\n    \"@bufbuild/protobuf\": \"^2.6.0\",\n    \"@connectrpc/connect\": \"^2.0.2\",\n    \"@connectrpc/connect-node\": \"^2.0.2\",\n    \"@electron-toolkit/preload\": \"^3.0.2\",\n    \"@electron-toolkit/utils\": \"^4.0.0\",\n    \"@emotion/react\": \"^11.14.0\",\n    \"@emotion/styled\": \"^11.14.0\",\n    \"@mui/lab\": \"^7.0.0-beta.13\",\n    \"@mui/material\": \"^7.1.1\",\n    \"@mynaui/icons-react\": \"^0.3.8\",\n    \"@opentelemetry/api\": \"^1.9.0\",\n    \"@opentelemetry/resources\": \"^1.25.0\",\n    \"@opentelemetry/sdk-node\": \"^0.54.0\",\n    \"@opentelemetry/semantic-conventions\": \"^1.25.0\",\n    \"@radix-ui/react-dialog\": \"^1.1.14\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.15\",\n    \"@radix-ui/react-slot\": \"^1.2.3\",\n    \"@radix-ui/react-switch\": \"^1.2.5\",\n    \"@radix-ui/react-tooltip\": \"^1.2.7\",\n    \"@sentry/electron\": \"^6.9.0\",\n    \"@sentry/vite-plugin\": \"^4.1.0\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"compromise\": \"^14.14.4\",\n    \"dotenv\": \"^17.0.1\",\n    \"electron-log\": \"^5.4.1\",\n    \"electron-store\": \"^8.1.0\",\n    \"electron-updater\": \"^6.6.8\",\n    \"jwt-decode\": \"^4.0.0\",\n    \"lucide-react\": \"^0.511.0\",\n    \"node-machine-id\": \"^1.1.12\",\n    \"posthog-js\": \"^1.269.1\",\n    \"react-router-dom\": \"^7.6.2\",\n    \"sqlite3\": \"^5.1.7\",\n    \"tailwind-merge\": \"^3.3.0\",\n    \"tw-animate-css\": \"^1.3.2\",\n    \"uuid\": \"^11.1.0\",\n    \"zustand\": \"^5.0.5\"\n  },\n  \"devDependencies\": {\n    \"@commitlint/cli\": \"^19.8.1\",\n    \"@commitlint/config-conventional\": \"^19.8.1\",\n    \"@electron-toolkit/eslint-config-prettier\": \"^3.0.0\",\n    \"@electron-toolkit/tsconfig\": \"^1.0.1\",\n    \"@electron/rebuild\": \"^4.0.1\",\n    \"@eslint/js\": \"^9.28.0\",\n    \"@rushstack/eslint-patch\": \"^1.11.0\",\n    \"@sinonjs/fake-timers\": \"^14.0.0\",\n    \"@tailwindcss/vite\": \"^4.1.8\",\n    \"@types/bun\": \"^1.2.19\",\n    \"@types/node\": \"^22.15.29\",\n    \"@types/react\": \"^19.1.6\",\n    \"@types/react-dom\": \"^19.1.5\",\n    \"@types/sinonjs__fake-timers\": \"^8.1.5\",\n    \"@vitejs/plugin-react\": \"^4.5.0\",\n    \"bun-types\": \"^1.2.19\",\n    \"electron\": \"^37.2.6\",\n    \"electron-builder\": \"^26.0.20\",\n    \"electron-vite\": \"^3.1.0\",\n    \"eslint\": \"^9.28.0\",\n    \"eslint-plugin-react\": \"^7.37.5\",\n    \"eslint-plugin-react-hooks\": \"^5.2.0\",\n    \"framer-motion\": \"^12.15.0\",\n    \"happy-dom\": \"^20.0.10\",\n    \"husky\": \"^9.1.7\",\n    \"lint-staged\": \"^16.1.2\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"prettier\": \"^3.5.3\",\n    \"react\": \"^19.1.0\",\n    \"react-dom\": \"^19.1.0\",\n    \"release-it\": \"^19.0.4\",\n    \"semantic-release\": \"^24.2.7\",\n    \"tailwindcss\": \"^4.1.8\",\n    \"typescript\": \"^5.8.3\",\n    \"typescript-eslint\": \"^8.33.0\",\n    \"vite\": \"^6.3.5\"\n  },\n  \"packageManager\": \"yarn@1.22.19+sha512.ff4579ab459bb25aa7c0ff75b62acebe576f6084b36aa842971cf250a5d8c6cd3bc9420b22ce63c7f93a0857bc6ef29291db39c3e7a23aab5adfd5a4dd6c5d71\"\n}\n"
  },
  {
    "path": "resources/build/entitlements.mac.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n    <true/>\n    <key>com.apple.security.cs.allow-dyld-environment-variables</key>\n    <true/>\n    <key>com.apple.security.device.audio-input</key>\n    <true/>\n    <key>com.apple.security.device.keyboard</key>\n    <true/>\n    <key>com.apple.security.automation.apple-events</key>\n    <true/>\n    <key>com.apple.security.screen-recording</key>\n    <true/>\n    <key>com.apple.security.temporary-exception.apple-events</key>\n    <array>\n        <string>com.apple.Terminal</string>\n        <string>com.apple.systempreferences</string>\n        <string>com.apple.systemevents</string>\n        <string>com.apple.systemuiserver</string>\n    </array>\n    <key>com.apple.security.automation.accessibility</key>\n    <true/>\n    <key>com.apple.security.temporary-exception.mach-lookup.global-name</key>\n    <array>\n        <string>com.apple.systemuiserver</string>\n    </array>\n    <key>com.apple.security.temporary-exception.listen-event</key>\n    <true/>\n    <key>com.apple.security.app-sandbox</key>\n    <false/>\n    <key>com.apple.security.cs.disable-library-validation</key>\n    <true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "scripts/clean-app-data.js",
    "content": "#!/usr/bin/env node\n\nconst os = require('os')\nconst fs = require('fs')\nconst path = require('path')\n\nconst platform = os.platform()\nlet appDataPath\n\nif (platform === 'darwin') {\n  appDataPath = path.join(os.homedir(), 'Library', 'Application Support', 'Ito')\n} else if (platform === 'win32') {\n  appDataPath = path.join(\n    process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),\n    'Ito',\n  )\n} else {\n  appDataPath = path.join(os.homedir(), '.config', 'ito')\n}\n\nif (fs.existsSync(appDataPath)) {\n  fs.rmSync(appDataPath, { recursive: true, force: true })\n  console.log(`✓ Removed app data from: ${appDataPath}`)\n} else {\n  console.log(`ℹ No app data found at: ${appDataPath}`)\n}\n"
  },
  {
    "path": "scripts/generate-constants.js",
    "content": "#!/usr/bin/env node\n\n/**\n * Script to generate TypeScript constants files from the root shared-constants.js\n * This ensures all parts of the monorepo use the same default values\n */\n\nconst fs = require('fs')\nconst path = require('path')\n\n// Import the shared constants\nconst { DEFAULT_ADVANCED_SETTINGS } = require('../shared-constants.js')\n\n// Template for generated TypeScript files\nconst generateTSFile = targetPath => `/* \n * AUTO-GENERATED FILE - DO NOT EDIT\n * Generated from /shared-constants.js\n * Run 'bun generate:constants' to regenerate\n */\n\nexport const DEFAULT_ADVANCED_SETTINGS = {\n  // ASR (Automatic Speech Recognition) settings\n  asrProvider: '${DEFAULT_ADVANCED_SETTINGS.asrProvider}',\n  asrModel: '${DEFAULT_ADVANCED_SETTINGS.asrModel}',\n  asrPrompt: \\`${DEFAULT_ADVANCED_SETTINGS.asrPrompt}\\`,\n  \n  // LLM (Large Language Model) settings\n  llmProvider: '${DEFAULT_ADVANCED_SETTINGS.llmProvider}',\n  llmModel: '${DEFAULT_ADVANCED_SETTINGS.llmModel}',\n  llmTemperature: ${DEFAULT_ADVANCED_SETTINGS.llmTemperature},\n  \n  // Prompt settings\n  transcriptionPrompt: \\`${DEFAULT_ADVANCED_SETTINGS.transcriptionPrompt.replace(/`/g, '\\\\`')}\\`,\n  editingPrompt: \\`${DEFAULT_ADVANCED_SETTINGS.editingPrompt.replace(/`/g, '\\\\`')}\\`,\n  \n  // Audio quality thresholds\n  noSpeechThreshold: ${DEFAULT_ADVANCED_SETTINGS.noSpeechThreshold},\n} as const;\n`\n\n// Paths to generate files\nconst targets = [\n  'lib/constants/generated-defaults.ts',\n  'server/src/constants/generated-defaults.ts',\n]\n\nconsole.log('🔄 Generating constants files...')\n\ntargets.forEach(target => {\n  const fullPath = path.join(__dirname, '..', target)\n  const dir = path.dirname(fullPath)\n\n  // Ensure directory exists\n  if (!fs.existsSync(dir)) {\n    fs.mkdirSync(dir, { recursive: true })\n  }\n\n  // Write the generated file\n  fs.writeFileSync(fullPath, generateTSFile(target))\n  console.log(`✅ Generated: ${target}`)\n})\n\n// Format the generated files using prettier\nconsole.log('🎨 Formatting generated files...')\nconst { execSync } = require('child_process')\n\ntargets.forEach(target => {\n  try {\n    execSync(`bunx prettier --write \"${target}\"`, { stdio: 'inherit' })\n    console.log(`✅ Formatted: ${target}`)\n  } catch (error) {\n    console.warn(`⚠️  Could not format ${target}:`, error.message)\n  }\n})\n\nconsole.log('🎉 Constants generation complete!')\n"
  },
  {
    "path": "server/.dockerignore",
    "content": "infra/\nnode_modules/\ndist \n.git/\n.env*\n.env\ntest/"
  },
  {
    "path": "server/Dockerfile",
    "content": "# Dockerfile\nFROM oven/bun:1-alpine AS base\n\nWORKDIR /\n\n# Copy package files\nCOPY package.json bun.lock ./\nRUN bun install\n\n# Copy source code\nCOPY . .\n\n# Make sure migration script is executable\nRUN chmod +x scripts/migrate.sh\n\n# Generate proto types during the build\nRUN bun run proto:gen:server\n\n# Build the typescript code using Bun's native TypeScript support\nRUN bun build src/index.ts --outdir dist --target bun\n\n# Expose the gRPC port\nEXPOSE 3000\n\n\n# Run the gRPC server with Bun\nCMD [\"bun\", \"run\", \"start\"]\n"
  },
  {
    "path": "server/README.md",
    "content": "# Ito Server\n\nThe Ito transcription server provides gRPC-based speech-to-text services for the Ito voice assistant application. This server handles audio transcription, user data management, and API authentication.\n\n## 🚀 Quick Start\n\n### Prerequisites\n\n- **Node.js 20+** with **Bun** package manager\n- **Docker & Docker Compose** (for local PostgreSQL database)\n- **GROQ API Key** (for transcription services)\n- **Auth0 Account** (optional, for authentication)\n\n### 1. Environment Setup\n\nCreate your environment configuration:\n\n```bash\n# Copy the example environment file\ncp .env.example .env\n```\n\nAdd the following configuration to your `.env` file:\n\n```bash\n# Database Configuration\nDB_HOST=localhost\nDB_PORT=5432\nDB_USER=devuser\nDB_PASS=devpass\nDB_NAME=devdb\n\n# Storage Configuration (S3/MinIO)\nBLOB_STORAGE_BUCKET=ito-audio-storage\nS3_ENDPOINT=http://localhost:9000\nS3_ACCESS_KEY_ID=minioadmin\nS3_SECRET_ACCESS_KEY=minioadmin\nS3_FORCE_PATH_STYLE=true\n\n# GROQ API Configuration (Required)\nGROQ_API_KEY=your_groq_api_key_here\n\n# CEREBRAS API Key (Not Required)\nCEREBRAS_API_KEY=your_CEREBRAS_API_KEY_here\n\n# Authentication (Optional - set to false for local development)\nREQUIRE_AUTH=false\nAUTH0_DOMAIN=your_auth0_domain.auth0.com\nAUTH0_AUDIENCE=http://localhost:3000\n\n# Billing (Stripe)\nSTRIPE_SECRET_KEY=sk_test_xxx\nSTRIPE_WEBHOOK_SECRET=whsec_xxx\nSTRIPE_PRICE_ID=price_xxx\nAPP_PROTOCOL=ito-dev\nSTRIPE_PUBLIC_BASE_URL=http://localhost:3000\n```\n\n### 2. Get Required API Keys\n\n#### GROQ API Key (Required)\n\n1. Visit [console.groq.com](https://console.groq.com)\n2. Create an account or sign in\n3. Navigate to **API Keys** section\n4. Create a new API key\n5. Copy the key to your `.env` file as `GROQ_API_KEY`\n\n#### Auth0 Setup (Optional)\n\nFor production or authenticated development:\n\n1. Create a [Auth0 account](https://auth0.com)\n2. Create a new application (API type)\n3. Copy **Domain** and **Audience** to your `.env` file\n4. Set `REQUIRE_AUTH=true` in your `.env`\n\n### 3. Install Dependencies\n\n```bash\nbun install\n```\n\n### 4. Local Services Setup\n\nStart all local services (PostgreSQL + MinIO for S3-compatible storage):\n\n```bash\n# Start all local services\nbun run local-services-up\n\n# Run database migrations\nbun run db:migrate\n```\n\nThis will start:\n\n- **PostgreSQL** on port 5432\n- **MinIO S3** on port 9000 (API) and 9001 (Console)\n- Auto-creates the `ito-audio-storage` bucket\n\n### 5. Start Development Server\n\n```bash\n# Start the server with hot reload\nbun run dev\n```\n\nThe server will start on `http://localhost:3000`\n\n## 📋 Available Scripts\n\n### Development\n\n```bash\nbun run dev              # Start development server with hot reload\nbun run start            # Start production server\nbun run build            # Build TypeScript to JavaScript\n```\n\n### Database & Storage Management\n\n```bash\n# Services Management\nbun run local-services-up    # Start PostgreSQL + MinIO\nbun run local-services-down  # Stop all local services\nbun run local-db-up          # Start only PostgreSQL\nbun run local-s3-up          # Start only MinIO + create bucket\nbun run local-s3-down        # Stop MinIO\n\n# Database Operations\nbun run db:migrate           # Run migrations up\nbun run db:migrate:down      # Run migrations down\nbun run db:migrate:create <name>  # Create new migration\n```\n\n### Protocol Buffers\n\n```bash\nbun run proto:gen        # Generate both server and client types\nbun run proto:gen:server # Generate server types only\nbun run proto:gen:client # Generate client types only\n```\n\n### Testing\n\n```bash\nbun run test-client      # Run gRPC client tests\n```\n\n## 🏗️ Architecture\n\n### Core Components\n\n- **Fastify Server**: HTTP/gRPC server with Auth0 integration\n- **Connect RPC**: Type-safe gRPC implementation\n- **PostgreSQL**: Primary database for user data and metadata\n- **S3/MinIO**: Audio file storage (local development uses MinIO)\n- **GROQ SDK**: AI transcription service integration\n\n### API Services\n\n#### 1. Transcription Service\n\n- `TranscribeFile`: Single file transcription\n- `TranscribeStream`: Real-time streaming transcription\n\n#### 2. Notes Service\n\n- Create, read, update, delete user notes\n- Automatic transcription saving\n\n#### 3. Dictionary Service\n\n- Custom vocabulary management\n- Pronunciation corrections\n\n#### 4. Interactions Service\n\n- Dictation session tracking\n- Usage analytics\n\n#### 5. User Data Service\n\n- Complete user data deletion\n- Privacy compliance\n\n## 🔧 Configuration\n\n### Environment Variables\n\n| Variable               | Required | Default     | Description                                 |\n| ---------------------- | -------- | ----------- | ------------------------------------------- |\n| `DB_HOST`              | Yes      | `localhost` | PostgreSQL host                             |\n| `DB_PORT`              | Yes      | `5432`      | PostgreSQL port                             |\n| `DB_USER`              | Yes      | -           | Database username                           |\n| `DB_PASS`              | Yes      | -           | Database password                           |\n| `DB_NAME`              | Yes      | -           | Database name                               |\n| `BLOB_STORAGE_BUCKET`  | Yes      | -           | S3 bucket name for audio storage            |\n| `S3_ENDPOINT`          | No       | -           | S3 endpoint (for MinIO/local development)   |\n| `S3_ACCESS_KEY_ID`     | No       | -           | S3 access key (for MinIO/local development) |\n| `S3_SECRET_ACCESS_KEY` | No       | -           | S3 secret key (for MinIO/local development) |\n| `S3_FORCE_PATH_STYLE`  | No       | `false`     | Use path-style S3 URLs (required for MinIO) |\n| `GROQ_API_KEY`         | Yes      | -           | GROQ API key for transcription              |\n| `CEREBRAS_API_KEY`     | No       | -           | CEREBRAS API key for reasoning              |\n| `REQUIRE_AUTH`         | No       | `false`     | Enable Auth0 authentication                 |\n| `AUTH0_DOMAIN`         | No\\*     | -           | Auth0 domain (\\*required if auth enabled)   |\n| `AUTH0_AUDIENCE`       | No\\*     | -           | Auth0 audience (\\*required if auth enabled) |\n\n### Database & Storage Configuration\n\n**PostgreSQL Database:**\nThe server uses PostgreSQL with automatic migrations. The database schema includes:\n\n- **users**: User profiles and settings\n- **notes**: Transcribed text and metadata\n- **interactions**: Dictation sessions (with S3 audio references)\n- **dictionary**: Custom vocabulary\n- **llm_settings**: User-specific LLM configuration\n\n**S3 Storage:**\nAudio files are stored in S3 (or MinIO for local development) with the following structure:\n\n- **Bucket**: Configured via `BLOB_STORAGE_BUCKET`\n- **Keys**: `raw-audio/{userId}/{audioUuid}`\n- **Format**: Raw audio bytes (no file extensions)\n\n**Local Development Setup:**\n\n- **MinIO Console**: http://localhost:9001 (admin/admin)\n- **MinIO S3 API**: http://localhost:9000\n- **Auto-bucket creation**: `ito-audio-storage` created automatically\n\n### Authentication\n\nAuthentication is optional for local development. When enabled:\n\n- All gRPC endpoints require valid JWT tokens\n- Auth0 provides user identity and authorization\n- User context is automatically injected into requests\n\n## 🚀 Production Deployment\n\n### Docker Deployment\n\n```bash\n# Build and start with Docker Compose\ndocker compose up -d\n\n# Run migrations\ndocker compose exec ito-grpc-server bun run db:migrate\n```\n\n### AWS Deployment\n\nThe server includes AWS CDK infrastructure:\n\n```bash\ncd infra\nnpm install\ncdk deploy --all\n```\n\nThis deploys:\n\n- ECS Fargate service\n- Application Load Balancer\n- Aurora Serverless PostgreSQL\n- Lambda functions for migrations\n\n## 🧪 Testing\n\n### Health Check\n\n```bash\ncurl http://localhost:3000/\n```\n\n### gRPC Testing\n\n```bash\n# Run the test client\nbun run test-client\n```\n\n### Manual Testing\n\nTest individual services using the included test client or tools like:\n\n- [grpcurl](https://github.com/fullstorydev/grpcurl)\n- [Postman](https://www.postman.com/) (with gRPC support)\n- [BloomRPC](https://github.com/bloomrpc/bloomrpc)\n\n## 🔍 Troubleshooting\n\n### Common Issues\n\n#### 1. Database Connection Errors\n\n```bash\n# Check if PostgreSQL & MinIO are running\nbun run local-services-up\n\n# Verify database credentials in .env\n# Ensure DB_HOST, DB_PORT, DB_USER, DB_PASS are correct\n```\n\n#### 2. GROQ API Errors\n\n```bash\n# Verify API key is valid\n# Check GROQ_API_KEY in .env file\n# Ensure you have credits in your GROQ account\n```\n\n#### 3. S3/MinIO Storage Issues\n\n```bash\n# Check if MinIO is running\nbun run local-s3-up\n\n# Reset MinIO data (WARNING: destroys stored files)\ndocker compose down -v\nbun run local-services-up\n\n# Manually setup MinIO bucket\n./scripts/setup-minio.sh\n\n# Verify S3 configuration in .env\n# Ensure S3_ENDPOINT, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY are set\n```\n\n#### 4. Migration Failures\n\n```bash\n# Reset migrations (WARNING: destroys data)\nbun run local-services-down\nbun run local-services-up\nbun run db:migrate\n```\n\n#### 5. Auth0 Configuration\n\n```bash\n# For local development, disable auth\necho \"REQUIRE_AUTH=false\" >> .env\n\n# For production, ensure AUTH0_DOMAIN and AUTH0_AUDIENCE are set\n```\n\n#### 6. Port Conflicts\n\nIf ports 5432, 9000, or 9001 are in use:\n\n- Modify ports in `docker-compose.yml`\n- Update corresponding environment variables\n\n### Debug Mode\n\nEnable verbose logging:\n\n```bash\nNODE_ENV=development bun run dev\n```\n\n### MinIO Console Access\n\nFor local development, access the MinIO console at http://localhost:9001:\n\n- **Username**: `minioadmin`\n- **Password**: `minioadmin`\n\nUse the console to:\n\n- View stored audio files\n- Monitor storage usage\n- Manage buckets and objects\n- Debug S3 operations\n\n### Logs\n\nCheck server logs for detailed error information:\n\n- Database connection issues\n- API authentication failures\n- Transcription service errors\n- Migration problems\n- S3 storage operations\n\n## 📚 API Documentation\n\n### Protocol Buffer Schema\n\nThe API is defined in `src/ito.proto`. Key services:\n\n```protobuf\nservice ItoService {\n  // Transcription\n  rpc TranscribeFile(TranscribeFileRequest) returns (TranscriptionResponse);\n  rpc TranscribeStream(stream AudioChunk) returns (TranscriptionResponse);\n\n  // Data Management\n  rpc CreateNote(CreateNoteRequest) returns (Note);\n  rpc ListNotes(ListNotesRequest) returns (ListNotesResponse);\n  // ... more services\n}\n```\n\n### Client Integration\n\nThe Ito desktop app automatically connects to `localhost:3000`. Ensure the server is running before starting the desktop application.\n\n## 🤝 Contributing\n\n1. **Fork and clone** the repository\n2. **Create feature branch** from `dev`\n3. **Set up development environment** following this guide\n4. **Make changes** with appropriate tests\n5. **Submit pull request** with clear description\n\n### Development Guidelines\n\n- Follow TypeScript best practices\n- Add migrations for schema changes\n- Test gRPC endpoints thoroughly\n- Update documentation for API changes\n- Consider backwards compatibility\n\n---\n\n## 📞 Support\n\n- **Issues**: [GitHub Issues](https://github.com/heyito/ito/issues)\n- **Documentation**: [Main README](../README.md)\n- **Server Logs**: Check console output for debugging information\n"
  },
  {
    "path": "server/buf.gen.yaml",
    "content": "version: v2\nplugins:\n  - local: protoc-gen-es\n    out: generated\n    opt:\n      - target=ts\n      - import_extension=.js\n  - local: protoc-gen-connect-es\n    out: generated\n    opt:\n      - target=ts\n      - import_extension=.js\ninputs:\n  - directory: src\n  - module: buf.build/bufbuild/protovalidate\n"
  },
  {
    "path": "server/buf.yaml",
    "content": "version: v2\nmodules:\n  - path: src\ndeps:\n  - buf.build/bufbuild/protovalidate\nlint:\n  use:\n    - DEFAULT\nbreaking:\n  use:\n    - FILE\n"
  },
  {
    "path": "server/docker-compose.yml",
    "content": "services:\n  ito-grpc-server:\n    build: .\n    ports:\n      - '$PORT:$PORT'\n    env_file:\n      - ./.env\n    volumes:\n      - .:/app\n      - /app/node_modules\n    command: bun run start\n    container_name: ito-server\n    restart: always\n    depends_on:\n      - db\n    environment:\n      NODE_ENV: development\n\n  db:\n    image: postgres:16\n    container_name: ito-postgres\n    restart: always\n    environment:\n      POSTGRES_USER: '$DB_USER'\n      POSTGRES_PASSWORD: '$DB_PASS'\n      POSTGRES_DB: '$DB_NAME'\n    ports:\n      - '$DB_PORT:$DB_PORT'\n    volumes:\n      - pgdata:/var/lib/postgresql/data\n\n  minio:\n    image: minio/minio:latest\n    container_name: ito-minio\n    restart: always\n    command: server /data --console-address \":9001\"\n    environment:\n      MINIO_ROOT_USER: '$S3_ACCESS_KEY_ID'\n      MINIO_ROOT_PASSWORD: '$S3_SECRET_ACCESS_KEY'\n    ports:\n      - '9000:9000' # S3 API\n      - '9001:9001' # MinIO Console\n    volumes:\n      - minio_data:/data\n    healthcheck:\n      test: ['CMD', 'mc', 'ready', 'local']\n      interval: 30s\n      timeout: 20s\n      retries: 3\n\nvolumes:\n  pgdata:\n  minio_data:\n"
  },
  {
    "path": "server/infra/.gitignore",
    "content": "*.js\n!jest.config.js\n*.d.ts\nnode_modules\nyarn.lock\nbun.lockb\n\n# CDK asset staging directory\n.cdk.staging\ncdk.out"
  },
  {
    "path": "server/infra/.npmignore",
    "content": "*.ts\n!*.d.ts\n\n# CDK asset staging directory\n.cdk.staging\ncdk.out\n"
  },
  {
    "path": "server/infra/README.md",
    "content": "# Welcome to your CDK TypeScript project\n\nThis is a blank project for CDK development with TypeScript.\n\nThe `cdk.json` file tells the CDK Toolkit how to execute your app.\n\n## Useful commands\n\n- `npm run build` compile typescript to js\n- `npm run watch` watch for changes and compile\n- `npm run test` perform the jest unit tests\n- `npx cdk deploy` deploy this stack to your default AWS account/region\n- `npx cdk diff` compare deployed stack with current state\n- `npx cdk synth` emits the synthesized CloudFormation template\n"
  },
  {
    "path": "server/infra/bin/infra.ts",
    "content": "import 'source-map-support/register'\nimport * as cdk from 'aws-cdk-lib'\nimport { Stage, StageProps } from 'aws-cdk-lib'\nimport { Construct } from 'constructs'\nimport { NetworkStack } from '../lib/network-stack'\nimport { PlatformStack } from '../lib/platform-stack'\nimport { ServiceStack } from '../lib/service-stack'\nimport { SecurityStack } from '../lib/security-stack'\nimport { ObservabilityStack } from '../lib/observability-stack'\nimport { ITO_PREFIX } from '../lib/constants'\nimport { GitHubOidcStack } from '../lib/cicd-stack'\n\nexport interface AppStageProps extends StageProps {\n  stageName: string\n}\n\nexport class AppStage extends Stage {\n  public readonly stageName: string\n\n  constructor(scope: Construct, id: string, props: AppStageProps) {\n    super(scope, id, props)\n\n    this.stageName = props.stageName\n\n    const network = new NetworkStack(this, `${ITO_PREFIX}Network`, {\n      env: props.env,\n    })\n\n    const platform = new PlatformStack(this, `${ITO_PREFIX}Platform`, {\n      env: props.env,\n      vpc: network.vpc,\n    })\n\n    const service = new ServiceStack(this, `${ITO_PREFIX}Service`, {\n      env: props.env,\n      vpc: network.vpc,\n      dbSecretArn: platform.dbSecretArn,\n      dbEndpoint: platform.dbEndpoint,\n      serviceRepo: platform.serviceRepo,\n      opensearchDomain: platform.opensearchDomain,\n      blobStorageBucket: platform.blobStorageBucket,\n      timingBucketName: platform.timingBucketName,\n    })\n\n    new SecurityStack(this, `${ITO_PREFIX}Security`, {\n      env: props.env,\n      fargateService: service.fargateService,\n      dbSecurityGroupId: platform.dbSecurityGroupId,\n    })\n\n    new ObservabilityStack(this, `${ITO_PREFIX}Observability`, {\n      env: props.env,\n      albFargate: service.albFargate,\n    })\n  }\n}\n\nconst app = new cdk.App()\n\nconst env = {\n  account: process.env.CDK_DEFAULT_ACCOUNT,\n  region: process.env.CDK_DEFAULT_REGION,\n}\n\nconst stages = ['dev', 'prod']\nstages.forEach(stageName => {\n  new AppStage(app, `${ITO_PREFIX}-${stageName}`, {\n    env,\n    stageName,\n  })\n})\n\nnew GitHubOidcStack(app, `${ITO_PREFIX}CiCd`, {\n  env,\n  stages,\n})\n\napp.synth()\n"
  },
  {
    "path": "server/infra/cdk.context.json",
    "content": "{\n  \"availability-zones:account=287641434880:region=us-west-2\": [\n    \"us-west-2a\",\n    \"us-west-2b\",\n    \"us-west-2c\",\n    \"us-west-2d\"\n  ],\n  \"hosted-zone:account=287641434880:domainName=ito-api.com:region=us-west-2\": {\n    \"Id\": \"/hostedzone/Z008510939MEEVRBSLOAJ\",\n    \"Name\": \"ito-api.com.\"\n  },\n  \"acknowledged-issue-numbers\": [34892, 34892, 34739]\n}\n"
  },
  {
    "path": "server/infra/cdk.json",
    "content": "{\n  \"app\": \"npx ts-node --prefer-ts-exts bin/infra.ts\",\n  \"watch\": {\n    \"include\": [\"**\"],\n    \"exclude\": [\n      \"README.md\",\n      \"cdk*.json\",\n      \"**/*.d.ts\",\n      \"**/*.js\",\n      \"tsconfig.json\",\n      \"package*.json\",\n      \"yarn.lock\",\n      \"node_modules\",\n      \"test\"\n    ]\n  },\n  \"context\": {\n    \"@aws-cdk/aws-lambda:recognizeLayerVersion\": true,\n    \"@aws-cdk/core:checkSecretUsage\": true,\n    \"@aws-cdk/core:target-partitions\": [\"aws\", \"aws-cn\"],\n    \"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver\": true,\n    \"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName\": true,\n    \"@aws-cdk/aws-ecs:arnFormatIncludesClusterName\": true,\n    \"@aws-cdk/aws-iam:minimizePolicies\": true,\n    \"@aws-cdk/core:validateSnapshotRemovalPolicy\": true,\n    \"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName\": true,\n    \"@aws-cdk/aws-s3:createDefaultLoggingPolicy\": true,\n    \"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption\": true,\n    \"@aws-cdk/aws-apigateway:disableCloudWatchRole\": true,\n    \"@aws-cdk/core:enablePartitionLiterals\": true,\n    \"@aws-cdk/aws-events:eventsTargetQueueSameAccount\": true,\n    \"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker\": true,\n    \"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName\": true,\n    \"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy\": true,\n    \"@aws-cdk/aws-route53-patters:useCertificate\": true,\n    \"@aws-cdk/customresources:installLatestAwsSdkDefault\": false,\n    \"@aws-cdk/aws-rds:databaseProxyUniqueResourceName\": true,\n    \"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup\": true,\n    \"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId\": true,\n    \"@aws-cdk/aws-ec2:launchTemplateDefaultUserData\": true,\n    \"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments\": true,\n    \"@aws-cdk/aws-redshift:columnId\": true,\n    \"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2\": true,\n    \"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup\": true,\n    \"@aws-cdk/aws-apigateway:requestValidatorUniqueId\": true,\n    \"@aws-cdk/aws-kms:aliasNameRef\": true,\n    \"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig\": true,\n    \"@aws-cdk/core:includePrefixInUniqueNameGeneration\": true,\n    \"@aws-cdk/aws-efs:denyAnonymousAccess\": true,\n    \"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby\": false,\n    \"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion\": true,\n    \"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId\": true,\n    \"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters\": true,\n    \"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier\": true,\n    \"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials\": true,\n    \"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource\": true,\n    \"@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction\": true,\n    \"@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse\": true,\n    \"@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2\": true,\n    \"@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope\": true,\n    \"@aws-cdk/aws-eks:nodegroupNameAttribute\": true,\n    \"@aws-cdk/aws-ec2:ebsDefaultGp3Volume\": true,\n    \"@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm\": true,\n    \"@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault\": false,\n    \"@aws-cdk/aws-s3:keepNotificationInImportedBucket\": false,\n    \"@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature\": false,\n    \"@aws-cdk/aws-ecs:disableEcsImdsBlocking\": true,\n    \"@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions\": true,\n    \"@aws-cdk/aws-dynamodb:resourcePolicyPerReplica\": true,\n    \"@aws-cdk/aws-ec2:ec2SumTImeoutEnabled\": true,\n    \"@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission\": true,\n    \"@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId\": true,\n    \"@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics\": true,\n    \"@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages\": true,\n    \"@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy\": true,\n    \"@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault\": true,\n    \"@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource\": true,\n    \"@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault\": true,\n    \"@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections\": true,\n    \"@aws-cdk/core:enableAdditionalMetadataCollection\": true,\n    \"@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy\": false,\n    \"@aws-cdk/aws-s3:setUniqueReplicationRoleName\": true,\n    \"@aws-cdk/aws-events:requireEventBusPolicySid\": true,\n    \"@aws-cdk/core:aspectPrioritiesMutating\": true,\n    \"@aws-cdk/aws-dynamodb:retainTableReplica\": true,\n    \"@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2\": true,\n    \"@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions\": true,\n    \"@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway\": true,\n    \"@aws-cdk/aws-s3:publicAccessBlockedByDefault\": true,\n    \"@aws-cdk/aws-lambda:useCdkManagedLogGroup\": true\n  }\n}\n"
  },
  {
    "path": "server/infra/jest.config.js",
    "content": "module.exports = {\n  testEnvironment: 'node',\n  roots: ['<rootDir>/test'],\n  testMatch: ['**/*.test.ts'],\n  transform: {\n    '^.+\\\\.tsx?$': 'ts-jest',\n  },\n}\n"
  },
  {
    "path": "server/infra/lambdas/firehose-transform.ts",
    "content": "import { gunzipSync } from 'zlib'\n\ntype FirehoseRecord = {\n  recordId: string\n  data: string\n}\n\ntype FirehoseResponseRecord = {\n  recordId: string\n  result: 'Ok' | 'Dropped' | 'ProcessingFailed'\n  data: string\n}\n\ntype FirehoseEvent = {\n  records: FirehoseRecord[]\n}\n\ntype CwLogEvent = {\n  id?: string\n  timestamp?: number\n  message?: unknown\n  owner?: string\n  logGroup?: string\n  logStream?: string\n  subscriptionFilters?: string[]\n}\n\ntype CwLogsBatch = {\n  messageType?: string\n  owner?: string\n  logGroup?: string\n  logStream?: string\n  subscriptionFilters?: string[]\n  logEvents?: { id: string; timestamp: number; message: string }[]\n}\n\nconst DATASET = process.env.DATASET || 'server'\nconst STAGE = process.env.STAGE || 'dev'\n\nfunction tryJsonParse<T>(text: unknown): T | undefined {\n  if (typeof text !== 'string') return undefined\n  const t = text.trim()\n  if (!t.startsWith('{') && !t.startsWith('[')) return undefined\n  try {\n    return JSON.parse(t) as T\n  } catch {\n    return undefined\n  }\n}\n\nfunction isGzip(buffer: Buffer): boolean {\n  return buffer.length >= 2 && buffer[0] === 0x1f && buffer[1] === 0x8b\n}\n\nfunction decodeRecord(b64: string): string {\n  const buf = Buffer.from(b64, 'base64')\n  const out = isGzip(buf) ? gunzipSync(buf) : buf\n  return out.toString('utf8')\n}\n\nfunction utf8ToBase64(s: string): string {\n  return Buffer.from(s, 'utf8').toString('base64')\n}\n\nfunction isoTimestamp(ms?: number): string {\n  const n = typeof ms === 'number' ? ms : Date.now()\n  return new Date(n).toISOString()\n}\n\nfunction clean(obj: Record<string, unknown>): Record<string, unknown> {\n  const out: Record<string, unknown> = {}\n  for (const [k, v] of Object.entries(obj)) {\n    if (v === undefined) continue\n    out[k] = v\n  }\n  return out\n}\n\nexport const handler = async (\n  event: FirehoseEvent,\n): Promise<{ records: FirehoseResponseRecord[] }> => {\n  const results: FirehoseResponseRecord[] = []\n\n  for (const rec of event.records) {\n    try {\n      const raw = decodeRecord(rec.data)\n      const wrapper = tryJsonParse<CwLogEvent | CwLogsBatch>(raw)\n\n      let logGroup: string | undefined\n      let logStream: string | undefined\n      let ts: number | undefined\n      let originalMessage: unknown = raw\n\n      if (wrapper && typeof wrapper === 'object') {\n        logGroup = (wrapper as any).logGroup\n        logStream = (wrapper as any).logStream\n        // CloudWatch Logs subscription payloads provide batched events\n        const batch = wrapper as CwLogsBatch\n        if (Array.isArray(batch.logEvents) && batch.logEvents.length > 0) {\n          if (batch.logEvents.length === 1) {\n            ts = batch.logEvents[0].timestamp\n            originalMessage = batch.logEvents[0].message\n          } else {\n            // Aggregate multiple events into a single message, earliest timestamp\n            ts = batch.logEvents.reduce(\n              (min, e) => (e.timestamp < min ? e.timestamp : min),\n              batch.logEvents[0].timestamp,\n            )\n            originalMessage = batch.logEvents.map(e => e.message).join('\\n')\n          }\n        } else {\n          // Legacy/single-event shape\n          const single = wrapper as CwLogEvent\n          ts = single.timestamp\n          originalMessage = single.message ?? raw\n        }\n      }\n\n      const messageStr =\n        typeof originalMessage === 'string'\n          ? originalMessage\n          : String(originalMessage)\n\n      const structured = tryJsonParse<any>(messageStr)\n\n      let doc: Record<string, unknown>\n\n      if (\n        structured &&\n        typeof structured === 'object' &&\n        ('message' in structured || 'level' in structured)\n      ) {\n        // Treat as structured client log\n        doc = clean({\n          '@timestamp': isoTimestamp(\n            typeof structured.ts === 'number' ? structured.ts : ts,\n          ),\n          'log.level': structured.level || 'info',\n          message: structured.message ?? messageStr,\n          fields: structured.fields || {},\n          'interaction.id': structured.interactionId,\n          'trace.id': structured.traceId,\n          'span.id': structured.spanId,\n          'service.name': 'ito',\n          'service.version': structured.appVersion,\n          platform: structured.platform,\n          'user.sub': structured.userSub,\n          'event.dataset': DATASET,\n          stage: STAGE,\n          'log.group': logGroup,\n          'log.stream': logStream,\n        })\n      } else {\n        // Console/plain log line\n        doc = clean({\n          '@timestamp': isoTimestamp(ts),\n          'log.level': 'info',\n          message: messageStr,\n          fields: {},\n          'event.dataset': DATASET,\n          stage: STAGE,\n          'service.name': 'ito',\n          'log.group': logGroup,\n          'log.stream': logStream,\n        })\n      }\n\n      const outLine = JSON.stringify(doc)\n      results.push({\n        recordId: rec.recordId,\n        result: 'Ok',\n        data: utf8ToBase64(outLine),\n      })\n    } catch {\n      results.push({\n        recordId: rec.recordId,\n        result: 'ProcessingFailed',\n        data: rec.data,\n      })\n    }\n  }\n\n  return { records: results }\n}\n"
  },
  {
    "path": "server/infra/lambdas/opensearch-bootstrap.ts",
    "content": "import https from 'https'\nimport crypto from 'crypto'\n\ntype Event = {\n  RequestType: 'Create' | 'Update' | 'Delete'\n}\n\nconst DOMAIN_ENDPOINT = process.env.DOMAIN_ENDPOINT!\nconst REGION = process.env.REGION || 'us-west-2'\nconst STAGE = process.env.STAGE || 'dev'\nconst isDev = STAGE === 'dev'\n\nconst templateSettings = {\n  number_of_shards: 1,\n  number_of_replicas: isDev ? 0 : 1,\n}\n\nconst rollover = {\n  min_index_age: '1d',\n  min_size: isDev ? '5gb' : '20gb',\n}\n\n// Minimal SigV4 signer for OpenSearch HTTP requests\nfunction signRequest(\n  method: string,\n  path: string,\n  body: string,\n  now: Date,\n  creds: {\n    accessKeyId: string\n    secretAccessKey: string\n    sessionToken?: string\n  },\n) {\n  const service = 'es'\n  const amzDate =\n    now.toISOString().replace(/[:-]|/g, '').replace(/\\..+/, '') + 'Z'\n  const date = amzDate.slice(0, 8)\n  const canonicalUri = path\n  const canonicalQuerystring = ''\n  const payloadHash = crypto\n    .createHash('sha256')\n    .update(body || '', 'utf8')\n    .digest('hex')\n  const canonicalHeaders =\n    `host:${DOMAIN_ENDPOINT}\\n` + `x-amz-date:${amzDate}\\n`\n  const signedHeaders = 'host;x-amz-date'\n  const canonicalRequest = [\n    method,\n    canonicalUri,\n    canonicalQuerystring,\n    canonicalHeaders,\n    signedHeaders,\n    payloadHash,\n  ].join('\\n')\n  const algorithm = 'AWS4-HMAC-SHA256'\n  const credentialScope = `${date}/${REGION}/${service}/aws4_request`\n  const stringToSign = [\n    algorithm,\n    amzDate,\n    credentialScope,\n    crypto.createHash('sha256').update(canonicalRequest, 'utf8').digest('hex'),\n  ].join('\\n')\n\n  const kDate = crypto\n    .createHmac('sha256', 'AWS4' + creds.secretAccessKey)\n    .update(date)\n    .digest()\n  const kRegion = crypto.createHmac('sha256', kDate).update(REGION).digest()\n  const kService = crypto.createHmac('sha256', kRegion).update(service).digest()\n  const kSigning = crypto\n    .createHmac('sha256', kService)\n    .update('aws4_request')\n    .digest()\n  const signature = crypto\n    .createHmac('sha256', kSigning)\n    .update(stringToSign, 'utf8')\n    .digest('hex')\n\n  const authorizationHeader = `${algorithm} Credential=${creds.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`\n\n  const headers: Record<string, string> = {\n    'content-type': 'application/json',\n    'x-amz-date': amzDate,\n    Authorization: authorizationHeader,\n  }\n  if (creds.sessionToken) headers['x-amz-security-token'] = creds.sessionToken\n  return headers\n}\n\nfunction request(path: string, method: string, body?: any): Promise<any> {\n  const data = body ? JSON.stringify(body) : ''\n  const now = new Date()\n  const accessKeyId = process.env.AWS_ACCESS_KEY_ID\n  const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY\n  const sessionToken = process.env.AWS_SESSION_TOKEN\n  if (!accessKeyId || !secretAccessKey) {\n    throw new Error('Missing AWS credentials for signing OpenSearch requests')\n  }\n  const headers = signRequest(method, path, data, now, {\n    accessKeyId,\n    secretAccessKey,\n    sessionToken,\n  })\n  if (data) {\n    headers['content-length'] = Buffer.byteLength(data).toString()\n  }\n\n  return new Promise((resolve, reject) => {\n    const req = https.request(\n      {\n        host: DOMAIN_ENDPOINT,\n        path,\n        method,\n        headers,\n      },\n      res => {\n        const chunks: Buffer[] = []\n        res.on('data', c => chunks.push(c))\n        res.on('end', () => {\n          const s = Buffer.concat(chunks).toString('utf8')\n          if (res.statusCode && res.statusCode >= 400) {\n            return reject(\n              new Error(`OpenSearch HTTP ${res.statusCode}: ${s || 'no body'}`),\n            )\n          }\n          try {\n            resolve(s ? JSON.parse(s) : {})\n          } catch {\n            resolve({})\n          }\n        })\n      },\n    )\n    req.on('error', reject)\n    if (data) req.write(data)\n    req.end()\n  })\n}\n\nconst clientTemplate = {\n  index_patterns: ['client-logs-*'],\n  template: {\n    settings: { index: templateSettings },\n    mappings: {\n      dynamic: false,\n      properties: {\n        '@timestamp': { type: 'date' },\n        'log.level': { type: 'keyword' },\n        message: {\n          type: 'text',\n          fields: { keyword: { type: 'keyword', ignore_above: 1024 } },\n        },\n        'event.dataset': { type: 'keyword' },\n        stage: { type: 'keyword' },\n        'service.name': { type: 'keyword' },\n        'service.version': { type: 'keyword' },\n        'log.group': { type: 'keyword' },\n        'log.stream': { type: 'keyword' },\n        'trace.id': { type: 'keyword' },\n        'span.id': { type: 'keyword' },\n        'interaction.id': { type: 'keyword' },\n        platform: { type: 'keyword' },\n        'user.sub': { type: 'keyword' },\n        fields: { type: 'object', enabled: false },\n      },\n    },\n  },\n}\n\nconst serverTemplate = {\n  index_patterns: ['server-logs-*'],\n  template: {\n    settings: { index: templateSettings },\n    mappings: {\n      dynamic: false,\n      properties: {\n        '@timestamp': { type: 'date' },\n        'log.level': { type: 'keyword' },\n        message: {\n          type: 'text',\n          fields: { keyword: { type: 'keyword', ignore_above: 1024 } },\n        },\n        'event.dataset': { type: 'keyword' },\n        stage: { type: 'keyword' },\n        'service.name': { type: 'keyword' },\n        'service.version': { type: 'keyword' },\n        'log.group': { type: 'keyword' },\n        'log.stream': { type: 'keyword' },\n        fields: { type: 'object', enabled: false },\n      },\n    },\n  },\n}\n\nconst timingAnalyticsTemplate = {\n  index_patterns: ['ito-timing-analytics-*'],\n  template: {\n    settings: { index: templateSettings },\n    mappings: {\n      dynamic: false,\n      properties: {\n        '@timestamp': { type: 'date' },\n        'event.dataset': { type: 'keyword' },\n        interaction_id: { type: 'keyword' },\n        user_id: { type: 'keyword' },\n        stage: { type: 'keyword' },\n        data_completeness: { type: 'keyword' }, // 'both', 'client_only', 'server_only'\n        client_received_at: { type: 'date' },\n        server_received_at: { type: 'date' },\n\n        // Client-specific metadata\n        client_metadata: {\n          type: 'object',\n          properties: {\n            platform: { type: 'keyword' },\n            app_version: { type: 'keyword' },\n            hostname: { type: 'keyword' },\n          },\n        },\n        client_total_duration_ms: { type: 'integer' },\n\n        // Server-specific metadata\n        server_metadata: {\n          type: 'object',\n          properties: {\n            // Extensible for future server-specific fields\n          },\n        },\n        server_total_duration_ms: { type: 'integer' },\n\n        // Unified events array (nested for proper querying)\n        events: {\n          type: 'nested',\n          properties: {\n            source: { type: 'keyword' }, // 'client' or 'server'\n            name: { type: 'keyword' },\n            start_ms: { type: 'long' },\n            end_ms: { type: 'long' },\n            duration_ms: { type: 'integer' },\n          },\n        },\n      },\n    },\n  },\n}\n\n// ISM policy to retain forever (no delete); rollover daily (or when large)\nconst ismPolicy = {\n  policy: {\n    description: 'Rollover daily, retain indefinitely',\n    default_state: 'hot',\n    states: [\n      {\n        name: 'hot',\n        actions: [{ rollover }],\n        transitions: [],\n      },\n    ],\n  },\n}\n\nexport const handler = async (_event: Event) => {\n  // Templates\n  await request('/_index_template/ito-client-logs', 'PUT', clientTemplate)\n  await request('/_index_template/ito-server-logs', 'PUT', serverTemplate)\n  await request(\n    '/_index_template/ito-timing-analytics',\n    'PUT',\n    timingAnalyticsTemplate,\n  )\n\n  // Apply ISM policy for both patterns\n  try {\n    await request(\n      '/_plugins/_ism/policies/ito-retain-forever',\n      'PUT',\n      ismPolicy,\n    )\n  } catch (e: any) {\n    const msg = typeof e?.message === 'string' ? e.message : `${e}`\n    if (!(msg.includes('HTTP 409') || msg.includes('\"status\":409'))) {\n      throw e\n    }\n  }\n\n  // Attach ISM policy via index template settings\n  const addPolicy = (t: any) => ({\n    ...t,\n    template: {\n      ...t.template,\n      settings: {\n        ...t.template.settings,\n        'index.plugins.index_state_management.policy_id': 'ito-retain-forever',\n      },\n    },\n  })\n  await request(\n    '/_index_template/ito-client-logs',\n    'PUT',\n    addPolicy(clientTemplate),\n  )\n  await request(\n    '/_index_template/ito-server-logs',\n    'PUT',\n    addPolicy(serverTemplate),\n  )\n  await request(\n    '/_index_template/ito-timing-analytics',\n    'PUT',\n    addPolicy(timingAnalyticsTemplate),\n  )\n\n  return { status: 'ok' }\n}\n"
  },
  {
    "path": "server/infra/lambdas/run-migration.ts",
    "content": "import * as aws from '@aws-sdk/client-ecs'\n\nconst ecs = new aws.ECS()\n\nexport const handler = async () => {\n  console.log('Migration ECS task started')\n\n  const { tasks } = await ecs.runTask({\n    cluster: process.env.CLUSTER!,\n    taskDefinition: process.env.TASK_DEF!,\n    launchType: 'FARGATE',\n    networkConfiguration: {\n      awsvpcConfiguration: {\n        subnets: process.env.SUBNETS!.split(','),\n        securityGroups: [process.env.SECURITY_GROUPS!],\n        assignPublicIp: 'DISABLED',\n      },\n    },\n    overrides: {\n      containerOverrides: [\n        {\n          name: process.env.CONTAINER_NAME!,\n          command: ['sh', './scripts/migrate.sh', 'up'],\n        },\n      ],\n    },\n  })\n\n  if (!tasks || tasks.length === 0) {\n    throw new Error('Failed to start ECS task')\n  }\n\n  const taskArn = tasks[0].taskArn\n  console.log(`Task started: ${taskArn}`)\n\n  let lastStatus\n  let attempts = 0\n  const maxAttempts = 100\n  while (true) {\n    if (attempts >= maxAttempts) {\n      throw new Error('Migration ECS task timed out')\n    }\n\n    const { tasks: describeTasks } = await ecs.describeTasks({\n      cluster: process.env.CLUSTER!,\n      tasks: [taskArn!],\n    })\n\n    const task = describeTasks?.[0]\n    if (!task) throw new Error('Could not describe ECS task')\n\n    lastStatus = task.lastStatus\n    console.log(`Current ECS task status: ${lastStatus}`)\n\n    if (lastStatus === 'STOPPED') {\n      const container = task.containers?.[0]\n      if (container?.exitCode !== 0) {\n        throw new Error(\n          `Migration failed with exit code ${container?.exitCode} (reason: ${container?.reason})`,\n        )\n      }\n\n      console.log('Migration ECS task completed successfully ✅')\n      break\n    }\n\n    attempts++\n    await new Promise(resolve => setTimeout(resolve, 5000)) // wait 5 sec before next check\n  }\n\n  return { success: true, message: 'Migration completed successfully' }\n}\n"
  },
  {
    "path": "server/infra/lambdas/timing-merger.ts",
    "content": "import { SQSEvent, SQSBatchItemFailure, S3Event } from 'aws-lambda'\nimport { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'\nimport { Client } from '@opensearch-project/opensearch'\nimport { AwsSigv4Signer } from '@opensearch-project/opensearch/aws'\nimport { defaultProvider } from '@aws-sdk/credential-provider-node'\n\nconst OPENSEARCH_ENDPOINT = process.env.OPENSEARCH_ENDPOINT\n\nif (!OPENSEARCH_ENDPOINT) {\n  throw new Error('OPENSEARCH_ENDPOINT environment variable is required')\n}\n\nfunction getTimingIndexName(): string {\n  const today = Intl.DateTimeFormat('en-CA').format(new Date())\n  return `ito-timing-analytics-${today}`\n}\n\n// Initialize clients\nconst s3Client = new S3Client({ region: process.env.AWS_REGION })\nconst osClient = new Client({\n  ...AwsSigv4Signer({\n    region: process.env.AWS_REGION || 'us-east-1',\n    service: 'es',\n    getCredentials: () => defaultProvider()(),\n  }),\n  node: `https://${OPENSEARCH_ENDPOINT}`,\n})\n\ninterface TimingEvent {\n  name: string\n  startMs: number\n  endMs?: number\n  durationMs?: number\n}\n\ninterface ClientTimingData {\n  source: 'client'\n  interactionId: string\n  userId: string\n  platform: string\n  appVersion: string\n  hostname: string\n  architecture: string\n  timestamp: string\n  totalDurationMs: number\n  events: TimingEvent[]\n}\n\ninterface ServerTimingData {\n  source: 'server'\n  interactionId: string\n  userId: string\n  timestamp: string\n  events: TimingEvent[]\n}\n\ntype TimingData = ClientTimingData | ServerTimingData\n\ninterface MergedEvent {\n  source: 'client' | 'server'\n  name: string\n  start_ms: number\n  end_ms?: number\n  duration_ms?: number\n}\n\nasync function getS3Object(bucket: string, key: string): Promise<string> {\n  const command = new GetObjectCommand({ Bucket: bucket, Key: key })\n  const response = await s3Client.send(command)\n\n  if (!response.Body) {\n    throw new Error(`No body in S3 object: ${bucket}/${key}`)\n  }\n\n  return await response.Body.transformToString()\n}\n\nasync function mergeAndUpsertTimingReport(\n  timingData: TimingData,\n): Promise<void> {\n  const { interactionId, source } = timingData\n\n  console.log(\n    `[TimingMerger] Processing ${source} timing for interaction: ${interactionId}`,\n  )\n\n  try {\n    const indexName = getTimingIndexName()\n\n    // Try to get existing document\n    const existing = await osClient\n      .get({\n        index: indexName,\n        id: interactionId,\n      })\n      .catch(() => null)\n\n    let mergedDoc: any\n\n    if (existing && existing.body._source) {\n      // Merge with existing document\n      const existingSource = existing.body._source\n      console.log(\n        `[TimingMerger] Found existing document for ${interactionId}, merging...`,\n      )\n\n      // Start with existing document structure\n      mergedDoc = {\n        ...existingSource,\n        '@timestamp':\n          existingSource['@timestamp'] ||\n          timingData.timestamp ||\n          new Date().toISOString(),\n        'event.dataset': 'ito-timing-analytics',\n        interaction_id: interactionId,\n        user_id: timingData.userId || existingSource.user_id,\n      }\n\n      // Get existing events or initialize empty array\n      const existingEvents: MergedEvent[] = existingSource.events || []\n\n      // Add new events with source tag\n      const newEvents: MergedEvent[] = timingData.events.map(e => ({\n        source: source,\n        name: e.name,\n        start_ms: e.startMs,\n        end_ms: e.endMs,\n        duration_ms: e.durationMs,\n      }))\n\n      // Combine events (filter out duplicates from same source)\n      const filteredExisting = existingEvents.filter(\n        (e: MergedEvent) => e.source !== source,\n      )\n      mergedDoc.events = [...filteredExisting, ...newEvents]\n\n      // Update data completeness\n      const allEvents = mergedDoc.events as MergedEvent[]\n      const hasClient = allEvents.some(\n        (e: MergedEvent) => e.source === 'client',\n      )\n      const hasServer = allEvents.some(\n        (e: MergedEvent) => e.source === 'server',\n      )\n      mergedDoc.data_completeness =\n        hasClient && hasServer\n          ? 'both'\n          : hasClient\n            ? 'client_only'\n            : 'server_only'\n\n      // Update source-specific metadata and track receipt time\n      if (source === 'client') {\n        const clientData = timingData as ClientTimingData\n        mergedDoc.client_metadata = {\n          platform: clientData.platform,\n          app_version: clientData.appVersion,\n          hostname: clientData.hostname,\n        }\n        mergedDoc.client_total_duration_ms = clientData.totalDurationMs\n        mergedDoc.client_received_at = new Date().toISOString()\n      } else {\n        mergedDoc.server_metadata = {\n          // Extensible for future server-specific fields\n        }\n        // Calculate server total duration from events\n        const serverTotalDuration =\n          timingData.events.length > 0\n            ? Math.max(...timingData.events.map(e => e.endMs || e.startMs)) -\n              Math.min(...timingData.events.map(e => e.startMs))\n            : 0\n        mergedDoc.server_total_duration_ms = serverTotalDuration\n        mergedDoc.server_received_at = new Date().toISOString()\n      }\n    } else {\n      // Create new document\n      console.log(\n        `[TimingMerger] Creating new document for ${interactionId}...`,\n      )\n\n      // Map events with source tag\n      const events: MergedEvent[] = timingData.events.map(e => ({\n        source: source,\n        name: e.name,\n        start_ms: e.startMs,\n        end_ms: e.endMs,\n        duration_ms: e.durationMs,\n      }))\n\n      // Use the timestamp from the report, or current time as fallback\n      const timestamp = timingData.timestamp || new Date().toISOString()\n\n      mergedDoc = {\n        '@timestamp': timestamp,\n        'event.dataset': 'ito-timing-analytics',\n        interaction_id: interactionId,\n        user_id: timingData.userId,\n        events: events,\n        data_completeness: source === 'client' ? 'client_only' : 'server_only',\n      }\n\n      // Track when each source's data arrived\n      if (source === 'client') {\n        mergedDoc.client_received_at = new Date().toISOString()\n      } else {\n        mergedDoc.server_received_at = new Date().toISOString()\n      }\n\n      // Add source-specific metadata\n      if (source === 'client') {\n        const clientData = timingData as ClientTimingData\n        mergedDoc.client_metadata = {\n          platform: clientData.platform,\n          app_version: clientData.appVersion,\n          hostname: clientData.hostname,\n        }\n        mergedDoc.client_total_duration_ms = clientData.totalDurationMs\n      } else {\n        mergedDoc.server_metadata = {\n          // Extensible for future server-specific fields\n        }\n        // Calculate server total duration from events\n        const serverTotalDuration =\n          timingData.events.length > 0\n            ? Math.max(...timingData.events.map(e => e.endMs || e.startMs)) -\n              Math.min(...timingData.events.map(e => e.startMs))\n            : 0\n        mergedDoc.server_total_duration_ms = serverTotalDuration\n      }\n    }\n\n    // Upsert the merged document\n    await osClient.index({\n      index: indexName,\n      id: interactionId,\n      body: mergedDoc,\n      refresh: false, // Don't wait for refresh for better performance\n    })\n\n    console.log(\n      `[TimingMerger] Successfully merged ${source} timing for interaction: ${interactionId}`,\n    )\n  } catch (error) {\n    console.error(\n      `[TimingMerger] Failed to merge timing report for ${interactionId}:`,\n      error,\n    )\n    throw error\n  }\n}\n\nexport const handler = async (\n  event: SQSEvent,\n): Promise<{ batchItemFailures: SQSBatchItemFailure[] }> => {\n  console.log(`[TimingMerger] Processing ${event.Records.length} SQS messages`)\n\n  const batchItemFailures: SQSBatchItemFailure[] = []\n\n  // Process each SQS message (which contains an S3 event)\n  await Promise.all(\n    event.Records.map(async sqsRecord => {\n      try {\n        // Parse S3 event from SQS message body\n        const s3Event = JSON.parse(sqsRecord.body) as S3Event\n\n        console.log(\n          `[TimingMerger] Processing SQS message with ${s3Event.Records.length} S3 events`,\n        )\n\n        // Process all S3 records in this message\n        for (const s3Record of s3Event.Records) {\n          try {\n            const bucket = s3Record.s3.bucket.name\n            const key = decodeURIComponent(\n              s3Record.s3.object.key.replace(/\\+/g, ' '),\n            )\n\n            console.log(`[TimingMerger] Processing S3 object: ${bucket}/${key}`)\n\n            // Read timing data from S3\n            const jsonContent = await getS3Object(bucket, key)\n            const timingData = JSON.parse(jsonContent) as TimingData\n\n            // Validate required fields\n            if (!timingData.interactionId || !timingData.source) {\n              console.error(\n                `[TimingMerger] Invalid timing data in ${key}: missing interactionId or source`,\n              )\n              // Invalid data is not retryable, skip it\n              continue\n            }\n\n            // Merge and upsert to OpenSearch\n            await mergeAndUpsertTimingReport(timingData)\n          } catch (error) {\n            console.error(\n              `[TimingMerger] Failed to process S3 record ${s3Record.s3.object.key}:`,\n              error,\n            )\n            // Mark this SQS message as failed so it can be retried\n            throw error\n          }\n        }\n      } catch (error) {\n        console.error(\n          `[TimingMerger] Failed to process SQS message ${sqsRecord.messageId}:`,\n          error,\n        )\n        // Add to batch item failures for retry\n        batchItemFailures.push({\n          itemIdentifier: sqsRecord.messageId,\n        })\n      }\n    }),\n  )\n\n  console.log(\n    `[TimingMerger] Finished processing batch. Failures: ${batchItemFailures.length}/${event.Records.length}`,\n  )\n\n  return { batchItemFailures }\n}\n"
  },
  {
    "path": "server/infra/lib/cicd-stack.ts",
    "content": "import { CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'\nimport { Construct } from 'constructs'\nimport {\n  OpenIdConnectProvider,\n  OpenIdConnectPrincipal,\n  Role,\n  PolicyStatement,\n  Effect,\n  AnyPrincipal,\n} from 'aws-cdk-lib/aws-iam'\nimport { Repository } from 'aws-cdk-lib/aws-ecr'\nimport {\n  CLUSTER_NAME,\n  DB_NAME,\n  ITO_PREFIX,\n  SERVER_NAME,\n  SERVICE_NAME,\n} from './constants'\nimport { BlockPublicAccess, Bucket } from 'aws-cdk-lib/aws-s3'\nimport { CachePolicy, Distribution } from 'aws-cdk-lib/aws-cloudfront'\nimport { S3BucketOrigin } from 'aws-cdk-lib/aws-cloudfront-origins'\n\nexport interface GitHubOidcStackProps extends StackProps {\n  stages: string[]\n}\n\nexport class GitHubOidcStack extends Stack {\n  constructor(scope: Construct, id: string, props: GitHubOidcStackProps) {\n    super(scope, id, props)\n\n    // ─── reference the existing GitHub OIDC provider ───────────────────────────\n    const oidcProviderArn = `arn:aws:iam::${this.account}:oidc-provider/token.actions.githubusercontent.com`\n    const oidc = OpenIdConnectProvider.fromOpenIdConnectProviderArn(\n      this,\n      'ImportedGitHubOidcProvider',\n      oidcProviderArn,\n    )\n\n    // ─── allow only workflows from your repo/org ────────────────────────────────\n    const principal = new OpenIdConnectPrincipal(oidc, {\n      StringLike: {\n        'token.actions.githubusercontent.com:sub': `repo:heyito/ito:*`,\n        'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',\n      },\n    })\n\n    // ─── create the CI/CD role ─────────────────────────────────────────────────\n    const ciCdRole = new Role(this, 'ItoGitHubCiCdRole', {\n      assumedBy: principal,\n      roleName: 'ItoGitHubCiCdRole',\n      description: 'GitHub Actions can assume this via OIDC',\n    })\n\n    // ─── create s3 bucket for releases ────────────────────────\n    props.stages.forEach(stage => {\n      const bucketName = `${stage}-${ITO_PREFIX.toLowerCase()}-releases`\n      const bucket = new Bucket(this, `${stage}-ItoReleasesBucket`, {\n        bucketName,\n        removalPolicy: RemovalPolicy.RETAIN,\n\n        blockPublicAccess: BlockPublicAccess.BLOCK_ACLS_ONLY,\n      })\n\n      bucket.grantReadWrite(ciCdRole)\n      bucket.grantPut(ciCdRole)\n      bucket.grantDelete(ciCdRole)\n\n      bucket.addToResourcePolicy(\n        new PolicyStatement({\n          effect: Effect.ALLOW,\n          principals: [new AnyPrincipal()],\n          actions: ['s3:GetObject'],\n          resources: [bucket.arnForObjects('releases/*')],\n        }),\n      )\n\n      const distribution = new Distribution(this, `${stage}-ReleasesCDN`, {\n        defaultBehavior: {\n          origin: S3BucketOrigin.withBucketDefaults(bucket, {\n            originPath: '/releases',\n          }),\n          cachePolicy: CachePolicy.CACHING_OPTIMIZED,\n        },\n      })\n\n      // Grant CloudFront invalidation permissions\n      ciCdRole.addToPolicy(\n        new PolicyStatement({\n          effect: Effect.ALLOW,\n          actions: [\n            'cloudfront:CreateInvalidation',\n            'cloudfront:GetInvalidation',\n            'cloudfront:ListInvalidations',\n          ],\n          resources: [distribution.distributionArn],\n        }),\n      )\n\n      new CfnOutput(this, `${stage}-ReleasesCDNUrl`, {\n        value: `https://${distribution.domainName}`,\n      })\n\n      new CfnOutput(this, `${stage}-ReleasesCDNId`, {\n        value: distribution.distributionId,\n      })\n    })\n\n    // ─── ECR: login + push ─────────────────────────────────────────────────────\n    ciCdRole.addToPolicy(\n      new PolicyStatement({\n        effect: Effect.ALLOW,\n        actions: ['ecr:GetAuthorizationToken'],\n        resources: ['*'], // auth token is account-wide\n      }),\n    )\n\n    props.stages.forEach(stage => {\n      const repoName = `${stage}-${SERVER_NAME}`\n      const repo = Repository.fromRepositoryName(this, `Repo${stage}`, repoName)\n      const repoArn = repo.repositoryArn\n      ciCdRole.addToPolicy(\n        new PolicyStatement({\n          effect: Effect.ALLOW,\n          actions: [\n            'ecr:BatchCheckLayerAvailability',\n            'ecr:BatchGetImage',\n            'ecr:GetDownloadUrlForLayer',\n            'ecr:InitiateLayerUpload',\n            'ecr:UploadLayerPart',\n            'ecr:CompleteLayerUpload',\n            'ecr:PutImage',\n            'ecr:DescribeRepositories',\n            'ecr:ListImages',\n          ],\n          resources: [repoArn, `${repoArn}/*`],\n        }),\n      )\n\n      ciCdRole.addToPolicy(\n        new PolicyStatement({\n          effect: Effect.ALLOW,\n          actions: [\n            'ecs:DescribeServices',\n            'ecs:UpdateService',\n            'ecs:ListClusters',\n            'ecs:DescribeClusters',\n          ],\n          resources: [\n            `arn:aws:ecs:${this.region}:${this.account}:cluster/${stage}-${CLUSTER_NAME}`,\n            `arn:aws:ecs:${this.region}:${this.account}:service/${stage}-${CLUSTER_NAME}/${stage}-${SERVICE_NAME}`,\n          ],\n        }),\n      )\n\n      ciCdRole.addToPolicy(\n        new PolicyStatement({\n          effect: Effect.ALLOW,\n          actions: [\n            'lambda:GetFunction',\n            'lambda:UpdateFunctionCode',\n            'lambda:UpdateFunctionConfiguration',\n            'lambda:InvokeFunction',\n          ],\n          resources: [\n            `arn:aws:lambda:${this.region}:${this.account}:function:${stage}-${DB_NAME}-migration`,\n          ],\n        }),\n      )\n    })\n\n    // ─── CloudFormation on any of our “Ito*” stacks ──────────────────────────────\n    const cfnArnPattern = `arn:aws:cloudformation:${this.region}:${this.account}:stack/${ITO_PREFIX}*/*`\n    ciCdRole.addToPolicy(\n      new PolicyStatement({\n        effect: Effect.ALLOW,\n        actions: [\n          'cloudformation:CreateChangeSet',\n          'cloudformation:ExecuteChangeSet',\n          'cloudformation:DeleteStack',\n          'cloudformation:DescribeStacks',\n          'cloudformation:GetTemplate',\n        ],\n        resources: [cfnArnPattern],\n      }),\n    )\n\n    // ─── CDK bootstrap version lookup (wildcard qualifier) ────────────────────\n    const ssmBootstrapBase = `arn:aws:ssm:${this.region}:${this.account}:parameter/cdk-bootstrap/*`\n    ciCdRole.addToPolicy(\n      new PolicyStatement({\n        effect: Effect.ALLOW,\n        actions: ['ssm:GetParameter', 'ssm:GetParameters'],\n        resources: [ssmBootstrapBase, `${ssmBootstrapBase}/*`],\n      }),\n    )\n\n    // ─── S3: publishing assets into the CDK assets bucket (wildcard bootstrap) ─\n    // bucket name pattern is: cdk-<qualifier>-assets-<acct>-<region>\n    const bucketPattern = `arn:aws:s3:::cdk-hnb*assets-${this.account}-${this.region}`\n    ciCdRole.addToPolicy(\n      new PolicyStatement({\n        effect: Effect.ALLOW,\n        actions: ['s3:GetBucketLocation', 's3:ListBucket', 's3:GetBucketAcl'],\n        resources: [bucketPattern],\n      }),\n    )\n    ciCdRole.addToPolicy(\n      new PolicyStatement({\n        effect: Effect.ALLOW,\n        actions: ['s3:GetObject', 's3:PutObject', 's3:PutObjectAcl'],\n        resources: [`${bucketPattern}/*`],\n      }),\n    )\n\n    // ─── allow CDK bootstrap roles to be assumed (wildcard qualifier) ──────────\n    const deployRolePattern = `arn:aws:iam::${this.account}:role/cdk-hnb*-deploy-role-${this.account}-${this.region}`\n    const publishRolePattern = `arn:aws:iam::${this.account}:role/cdk-hnb*-file-publishing-role-${this.account}-${this.region}`\n    ciCdRole.addToPolicy(\n      new PolicyStatement({\n        effect: Effect.ALLOW,\n        actions: ['sts:AssumeRole'],\n        resources: [deployRolePattern, publishRolePattern],\n      }),\n    )\n  }\n}\n"
  },
  {
    "path": "server/infra/lib/constants.ts",
    "content": "export const DB_NAME = 'ItoDb'\nexport const SERVER_NAME = 'ito-server'\nexport const CLUSTER_NAME = 'ito-cluster'\nexport const SERVICE_NAME = 'ito-service'\nexport const ITO_PREFIX = 'Ito'\nexport const SERVICE_REPO_ARN = 'ServiceRepoArn'\nexport const DB_PORT = 5432\n"
  },
  {
    "path": "server/infra/lib/helpers.ts",
    "content": "export function isDev(stage: string) {\n  return stage === 'dev'\n}\n"
  },
  {
    "path": "server/infra/lib/network-stack.ts",
    "content": "import { Stack, StackProps } from 'aws-cdk-lib'\nimport { Vpc, GatewayVpcEndpointAwsService } from 'aws-cdk-lib/aws-ec2'\nimport { Construct } from 'constructs'\n\nexport class NetworkStack extends Stack {\n  public readonly vpc: Vpc\n\n  constructor(scope: Construct, id: string, props?: StackProps) {\n    super(scope, id, props)\n    this.vpc = new Vpc(this, 'ItoVpc', {\n      maxAzs: 2,\n      natGateways: 2,\n    })\n\n    // Add S3 Gateway Endpoint to avoid NAT Gateway charges for S3 traffic\n    // This is free and provides better performance for S3 access\n    this.vpc.addGatewayEndpoint('S3Endpoint', {\n      service: GatewayVpcEndpointAwsService.S3,\n    })\n  }\n}\n"
  },
  {
    "path": "server/infra/lib/observability-stack.ts",
    "content": "import { Stack, StackProps, Stage, Tags } from 'aws-cdk-lib'\nimport { Alarm } from 'aws-cdk-lib/aws-cloudwatch'\nimport { Construct } from 'constructs'\nimport { AppStage } from '../bin/infra'\nimport { Topic } from 'aws-cdk-lib/aws-sns'\nimport { SnsAction } from 'aws-cdk-lib/aws-cloudwatch-actions'\nimport { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns'\n\nexport interface ObservabilityStackProps extends StackProps {\n  albFargate: ApplicationLoadBalancedFargateService\n}\n\nexport class ObservabilityStack extends Stack {\n  public readonly alertTopic: Topic\n\n  constructor(scope: Construct, id: string, props: ObservabilityStackProps) {\n    super(scope, id, props)\n\n    const stage = Stage.of(this) as AppStage\n    const stageName = stage.stageName\n\n    const alertTopic = new Topic(this, 'ItoAlarmTopic', {\n      topicName: `${stageName}-ito-alarms`,\n      displayName: `Ito service alarms for ${stageName}`,\n    })\n    this.alertTopic = alertTopic\n\n    const snsAction = new SnsAction(alertTopic)\n\n    new Alarm(this, `${stageName}-HighItoFargateCpu`, {\n      metric: props.albFargate.service.metricCpuUtilization(),\n      threshold: 80,\n      evaluationPeriods: 2,\n      datapointsToAlarm: 2,\n      alarmDescription: 'Fargate CPU > 80% for 2 consecutive periods',\n      actionsEnabled: true,\n      alarmName: `${stageName}-ito-cpu-high`,\n    }).addAlarmAction(snsAction)\n\n    new Alarm(this, `${stageName}-HighItoFargateMemory`, {\n      metric: props.albFargate.service.metricMemoryUtilization(),\n      threshold: 75,\n      evaluationPeriods: 2,\n      datapointsToAlarm: 2,\n      alarmDescription: 'Fargate Memory > 75% for 2 consecutive periods',\n      actionsEnabled: true,\n      alarmName: `${stageName}-ito-memory-high`,\n    }).addAlarmAction(snsAction)\n\n    new Alarm(this, `${stageName}-ItoUnhealthyTasks`, {\n      metric: props.albFargate.targetGroup.metrics.unhealthyHostCount(),\n      threshold: 1,\n      evaluationPeriods: 1,\n      alarmDescription: 'There is at least 1 unhealthy task in the service',\n      actionsEnabled: true,\n      alarmName: `${stageName}-ito-unhealthy-tasks`,\n    }).addAlarmAction(snsAction)\n\n    Tags.of(this).add('Project', 'Ito')\n  }\n}\n"
  },
  {
    "path": "server/infra/lib/platform-stack.ts",
    "content": "import {\n  CfnOutput,\n  Duration,\n  RemovalPolicy,\n  Stack,\n  StackProps,\n  Stage,\n  Tags,\n} from 'aws-cdk-lib'\nimport { SecurityGroup, Vpc, EbsDeviceVolumeType } from 'aws-cdk-lib/aws-ec2'\nimport { BlockPublicAccess, Bucket } from 'aws-cdk-lib/aws-s3'\nimport {\n  AuroraPostgresEngineVersion,\n  ClusterInstance,\n  Credentials,\n  DatabaseCluster,\n  DatabaseClusterEngine,\n} from 'aws-cdk-lib/aws-rds'\nimport { Secret } from 'aws-cdk-lib/aws-secretsmanager'\nimport { Construct } from 'constructs'\nimport { DB_NAME, SERVER_NAME } from './constants'\nimport { Repository } from 'aws-cdk-lib/aws-ecr'\nimport { AppStage } from '../bin/infra'\nimport { isDev } from './helpers'\nimport {\n  Domain,\n  EngineVersion,\n  TLSSecurityPolicy,\n} from 'aws-cdk-lib/aws-opensearchservice'\nimport {\n  AccountRootPrincipal,\n  Effect,\n  PolicyStatement,\n  ServicePrincipal,\n  ArnPrincipal,\n  Role,\n  ManagedPolicy,\n  FederatedPrincipal,\n} from 'aws-cdk-lib/aws-iam'\nimport {\n  UserPool,\n  UserPoolDomain,\n  CfnIdentityPool,\n  UserPoolClient,\n  CfnIdentityPoolRoleAttachment,\n} from 'aws-cdk-lib/aws-cognito'\nimport { createTimingInfrastructure } from './timing-config'\n\nexport interface PlatformStackProps extends StackProps {\n  vpc: Vpc\n}\n\nexport class PlatformStack extends Stack {\n  public readonly dbSecretArn: string\n  public readonly dbEndpoint: string\n  public readonly dbSecurityGroupId: string\n  public readonly serviceRepo: Repository\n  public readonly opensearchDomain: Domain\n  public readonly blobStorageBucket: Bucket\n  public readonly timingBucketName: string\n\n  constructor(scope: Construct, id: string, props: PlatformStackProps) {\n    super(scope, id, props)\n\n    const stage = Stage.of(this) as AppStage\n    const stageName = stage.stageName\n\n    const dbSecurityGroup = new SecurityGroup(this, 'ItoDbSecurityGroup', {\n      vpc: props.vpc,\n      description: 'Allow ECS Fargate service to connect to Aurora',\n      allowAllOutbound: true,\n    })\n\n    const dbCredentialsSecret = new Secret(this, 'ItoDbCredentials', {\n      secretName: `${stageName}/ito-db/dbadmin`,\n      generateSecretString: {\n        secretStringTemplate: JSON.stringify({ username: 'dbadmin' }),\n        excludePunctuation: true,\n        includeSpace: false,\n        generateStringKey: 'password',\n      },\n    })\n\n    this.dbSecretArn = dbCredentialsSecret.secretArn\n\n    const dbCluster = new DatabaseCluster(this, 'ItoAuroraServerless', {\n      engine: DatabaseClusterEngine.auroraPostgres({\n        version: AuroraPostgresEngineVersion.VER_16_2,\n      }),\n      enablePerformanceInsights: true,\n      vpc: props.vpc,\n      securityGroups: [dbSecurityGroup],\n      credentials: Credentials.fromSecret(dbCredentialsSecret),\n      defaultDatabaseName: `${DB_NAME}`,\n      clusterIdentifier: `${stageName}-${DB_NAME}Cluster`,\n      writer: ClusterInstance.serverlessV2('WriterInstance'),\n      readers: [\n        ClusterInstance.serverlessV2('ReaderInstance', {\n          scaleWithWriter: true,\n        }),\n      ],\n      serverlessV2MinCapacity: isDev(stageName) ? 0.5 : 2,\n      serverlessV2MaxCapacity: isDev(stageName) ? 4 : 10,\n      backup: {\n        retention: Duration.days(7),\n      },\n      removalPolicy: isDev(stageName)\n        ? RemovalPolicy.DESTROY\n        : RemovalPolicy.RETAIN,\n    })\n\n    this.dbEndpoint = dbCluster.clusterEndpoint.hostname\n    this.dbSecurityGroupId = dbSecurityGroup.securityGroupId\n\n    new CfnOutput(this, 'DbEndpoint', {\n      value: dbCluster.clusterEndpoint.socketAddress,\n    })\n\n    this.serviceRepo = new Repository(this, 'ItoServiceRepo', {\n      repositoryName: `${stageName}-${SERVER_NAME}`,\n      removalPolicy: isDev(stageName)\n        ? RemovalPolicy.DESTROY\n        : RemovalPolicy.RETAIN,\n      lifecycleRules: [{ maxImageCount: 20 }],\n    })\n\n    // Blob storage bucket for storing user-uploaded files and data\n    this.blobStorageBucket = new Bucket(this, 'ItoBlobStorage', {\n      bucketName: `${stageName}-${this.account}-${this.region}-ito-blob-storage`,\n      removalPolicy: isDev(stageName)\n        ? RemovalPolicy.DESTROY\n        : RemovalPolicy.RETAIN,\n      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,\n      enforceSSL: true,\n      versioned: true,\n    })\n\n    // Firehose role is created in the platform stack so the OpenSearch domain\n    // resource policy can reference a stable principal without cross-stack timing issues\n    const firehoseRole = new Role(this, 'ItoFirehoseRole', {\n      assumedBy: new ServicePrincipal('firehose.amazonaws.com'),\n      roleName: `${stageName}-ItoFirehoseRole`,\n    })\n\n    // Cognito resources for OpenSearch Dashboards authentication\n    const userPool = new UserPool(this, 'ItoOsUserPool', {\n      userPoolName: `${stageName}-ito-os-userpool`,\n      selfSignUpEnabled: true,\n      signInAliases: { email: true, username: true },\n      accountRecovery: undefined,\n    })\n    new UserPoolClient(this, 'ItoOsUserPoolClient', {\n      userPool,\n      userPoolClientName: `${stageName}-ito-os-client`,\n      generateSecret: false,\n      authFlows: { userSrp: true, userPassword: true },\n    })\n    new UserPoolDomain(this, 'ItoOsUserPoolDomain', {\n      userPool,\n      cognitoDomain: {\n        domainPrefix: `${stageName}-${this.account}-ito-os`.toLowerCase(),\n      },\n    })\n    const identityPool = new CfnIdentityPool(this, 'ItoOsIdentityPool', {\n      allowUnauthenticatedIdentities: true,\n      identityPoolName: `${stageName}-ito-os-identitypool`,\n      // Leave providers empty so OpenSearch can register its own App Client\n    })\n    const authenticatedRole = new Role(this, 'ItoOsCognitoAuthenticatedRole', {\n      assumedBy: new FederatedPrincipal(\n        'cognito-identity.amazonaws.com',\n        {\n          StringEquals: {\n            'cognito-identity.amazonaws.com:aud': identityPool.ref,\n          },\n          'ForAnyValue:StringLike': {\n            'cognito-identity.amazonaws.com:amr': 'authenticated',\n          },\n        },\n        'sts:AssumeRoleWithWebIdentity',\n      ),\n    })\n    const unauthenticatedRole = new Role(\n      this,\n      'ItoOsCognitoUnauthenticatedRole',\n      {\n        assumedBy: new FederatedPrincipal(\n          'cognito-identity.amazonaws.com',\n          {\n            StringEquals: {\n              'cognito-identity.amazonaws.com:aud': identityPool.ref,\n            },\n            'ForAnyValue:StringLike': {\n              'cognito-identity.amazonaws.com:amr': 'unauthenticated',\n            },\n          },\n          'sts:AssumeRoleWithWebIdentity',\n        ),\n      },\n    )\n    // Attach the authenticated role to the identity pool\n    new CfnIdentityPoolRoleAttachment(this, 'ItoOsIdentityPoolRoleAttachment', {\n      identityPoolId: identityPool.ref,\n      roles: {\n        authenticated: authenticatedRole.roleArn,\n        unauthenticated: unauthenticatedRole.roleArn,\n      },\n    })\n    const cognitoAccessRole = new Role(this, 'ItoOsCognitoAccessRole', {\n      assumedBy: new ServicePrincipal('opensearchservice.amazonaws.com'),\n      managedPolicies: [\n        ManagedPolicy.fromAwsManagedPolicyName(\n          'AmazonOpenSearchServiceCognitoAccess',\n        ),\n      ],\n    })\n\n    // OpenSearch domain for logs (one per stage)\n    const domain = new Domain(this, 'ItoLogsDomain', {\n      domainName: `${stageName}-ito-logs`,\n      version: EngineVersion.OPENSEARCH_2_13,\n      enforceHttps: true,\n      nodeToNodeEncryption: true,\n      encryptionAtRest: { enabled: true },\n      fineGrainedAccessControl: {\n        masterUserArn: `arn:aws:iam::${this.account}:root`,\n      },\n      cognitoDashboardsAuth: {\n        userPoolId: userPool.userPoolId,\n        identityPoolId: identityPool.ref,\n        role: cognitoAccessRole,\n      },\n      ebs: {\n        enabled: true,\n        volumeSize: isDev(stageName) ? 20 : 50,\n        volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3,\n      },\n      capacity: {\n        dataNodes: isDev(stageName) ? 1 : 2,\n        dataNodeInstanceType: 'm7g.large.search',\n        multiAzWithStandbyEnabled: false,\n      },\n      zoneAwareness: isDev(stageName)\n        ? { enabled: false }\n        : { enabled: true, availabilityZoneCount: 2 },\n      tlsSecurityPolicy: TLSSecurityPolicy.TLS_1_2,\n    })\n    domain.applyRemovalPolicy(\n      isDev(stageName) ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN,\n    )\n    domain.addAccessPolicies(\n      new PolicyStatement({\n        effect: Effect.ALLOW,\n        principals: [new ArnPrincipal(firehoseRole.roleArn)],\n        actions: ['es:ESHttp*'],\n        resources: [domain.domainArn, `${domain.domainArn}/*`],\n      }),\n    )\n\n    // Also allow the Firehose service principal gated by SourceAccount/SourceArn\n    domain.addAccessPolicies(\n      new PolicyStatement({\n        effect: Effect.ALLOW,\n        principals: [new ServicePrincipal('firehose.amazonaws.com')],\n        actions: ['es:ESHttp*'],\n        resources: [domain.domainArn, `${domain.domainArn}/*`],\n        conditions: {\n          StringEquals: { 'aws:SourceAccount': this.account },\n          ArnLike: {\n            'aws:SourceArn': [\n              `arn:aws:firehose:${this.region}:${this.account}:deliverystream/${stageName}-ito-client-logs`,\n              `arn:aws:firehose:${this.region}:${this.account}:deliverystream/${stageName}-ito-server-logs`,\n            ],\n          },\n        },\n      }),\n    )\n\n    // Allow any IAM principal from this AWS account to access the domain via IAM (SigV4)\n    // Using AccountRootPrincipal is the recommended way to grant all users/roles in the account\n    domain.addAccessPolicies(\n      new PolicyStatement({\n        effect: Effect.ALLOW,\n        principals: [new AccountRootPrincipal()],\n        actions: ['es:ESHttp*'],\n        resources: [domain.domainArn, `${domain.domainArn}/*`],\n      }),\n    )\n\n    // Allow Cognito authenticated users (role assumed via Identity Pool) to use Dashboards\n    domain.addAccessPolicies(\n      new PolicyStatement({\n        effect: Effect.ALLOW,\n        principals: [new ArnPrincipal(authenticatedRole.roleArn)],\n        actions: ['es:ESHttp*'],\n        resources: [domain.domainArn, `${domain.domainArn}/*`],\n      }),\n    )\n    this.opensearchDomain = domain\n\n    // Create timing infrastructure (S3 + Lambda merger)\n    const timingResources = createTimingInfrastructure(this, {\n      stageName,\n      opensearchDomain: domain,\n      accountId: this.account,\n      region: this.region,\n    })\n    this.timingBucketName = timingResources.timingBucket.bucketName\n\n    new CfnOutput(this, 'OpenSearchEndpoint', {\n      value: domain.domainEndpoint,\n    })\n\n    new CfnOutput(this, 'BlobStorageBucketArn', {\n      value: this.blobStorageBucket.bucketArn,\n    })\n\n    new CfnOutput(this, 'TimingBucketName', {\n      value: this.timingBucketName,\n      description: 'S3 bucket for raw timing analytics data',\n    })\n\n    new CfnOutput(this, 'TimingQueueUrl', {\n      value: timingResources.timingQueue.queueUrl,\n      description: 'SQS queue for timing events',\n    })\n\n    new CfnOutput(this, 'TimingDLQUrl', {\n      value: timingResources.timingDLQ.queueUrl,\n      description: 'Dead letter queue for failed timing events',\n    })\n\n    Tags.of(this).add('Project', 'Ito')\n  }\n}\n"
  },
  {
    "path": "server/infra/lib/security-stack.ts",
    "content": "import { Stack, StackProps, Tags } from 'aws-cdk-lib'\nimport { Peer, Port, SecurityGroup } from 'aws-cdk-lib/aws-ec2'\nimport { FargateService } from 'aws-cdk-lib/aws-ecs'\nimport { Construct } from 'constructs'\nimport { DB_PORT } from './constants'\n\nexport interface SecurityStackProps extends StackProps {\n  fargateService: FargateService\n  dbSecurityGroupId: string\n}\n\nexport class SecurityStack extends Stack {\n  constructor(scope: Construct, id: string, props: SecurityStackProps) {\n    super(scope, id, props)\n\n    const dbSG = SecurityGroup.fromSecurityGroupId(\n      this,\n      'ImportedDbSG',\n      props.dbSecurityGroupId,\n    )\n\n    dbSG.addIngressRule(\n      Peer.securityGroupId(\n        props.fargateService.connections.securityGroups[0].securityGroupId,\n      ),\n      Port.tcp(DB_PORT),\n      'Allow app to connect to aurora',\n    )\n\n    Tags.of(this).add('Project', 'Ito')\n  }\n}\n"
  },
  {
    "path": "server/infra/lib/service/fargate-task.ts",
    "content": "import { Construct } from 'constructs'\nimport { Stack } from 'aws-cdk-lib'\nimport {\n  ContainerImage,\n  CpuArchitecture,\n  Secret as EcsSecret,\n  FargateTaskDefinition,\n  OperatingSystemFamily,\n  AwsLogDriver,\n} from 'aws-cdk-lib/aws-ecs'\nimport {\n  Role as IamRole,\n  ServicePrincipal,\n  ManagedPolicy,\n  Policy,\n  PolicyStatement,\n} from 'aws-cdk-lib/aws-iam'\nimport { ISecret } from 'aws-cdk-lib/aws-secretsmanager'\nimport { Repository } from 'aws-cdk-lib/aws-ecr'\nimport { ILogGroup } from 'aws-cdk-lib/aws-logs'\nimport { isDev } from '../helpers'\n\nexport interface FargateTaskConfig {\n  stageName: string\n  serviceRepo: Repository\n  dbCredentialsSecret: ISecret\n  groqApiKeySecret: ISecret\n  cerebrasApiKeySecret: ISecret\n  stripeSecretKeySecret: ISecret\n  stripeWebhookSecret: ISecret\n  dbEndpoint: string\n  dbName: string\n  dbPort: number\n  domainName: string\n  clientLogGroup: ILogGroup\n  serverLogGroup: ILogGroup\n  blobStorageBucketName?: string\n  timingBucketName?: string\n}\n\nexport interface FargateTaskResources {\n  taskDefinition: FargateTaskDefinition\n  taskRole: IamRole\n  taskExecutionRole: IamRole\n  containerName: string\n  taskLogsPolicy: Policy\n}\n\nexport function createFargateTask(\n  scope: Construct,\n  config: FargateTaskConfig,\n): FargateTaskResources {\n  const stack = Stack.of(scope)\n\n  const fargateTaskRole = new IamRole(scope, 'ItoFargateTaskRole', {\n    assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'),\n  })\n\n  config.dbCredentialsSecret.grantRead(fargateTaskRole)\n  config.groqApiKeySecret.grantRead(fargateTaskRole)\n  config.cerebrasApiKeySecret.grantRead(fargateTaskRole)\n  config.stripeSecretKeySecret.grantRead(fargateTaskRole)\n  config.stripeWebhookSecret.grantRead(fargateTaskRole)\n\n  const taskExecutionRole = new IamRole(scope, 'ItoTaskExecRole', {\n    assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'),\n    managedPolicies: [\n      ManagedPolicy.fromAwsManagedPolicyName(\n        'service-role/AmazonECSTaskExecutionRolePolicy',\n      ),\n    ],\n  })\n\n  // Grant execution role permissions to read secrets (required for ECS to pull secrets during container startup)\n  config.dbCredentialsSecret.grantRead(taskExecutionRole)\n  config.groqApiKeySecret.grantRead(taskExecutionRole)\n  config.cerebrasApiKeySecret.grantRead(taskExecutionRole)\n  config.stripeSecretKeySecret.grantRead(taskExecutionRole)\n  config.stripeWebhookSecret.grantRead(taskExecutionRole)\n\n  // Explicitly add policy statement for secrets to ensure permissions are applied correctly\n  // This is a workaround for cases where grantRead() might not work correctly with fromSecretNameV2()\n  taskExecutionRole.addToPolicy(\n    new PolicyStatement({\n      actions: [\n        'secretsmanager:GetSecretValue',\n        'secretsmanager:DescribeSecret',\n      ],\n      resources: [\n        config.dbCredentialsSecret.secretArn,\n        config.groqApiKeySecret.secretArn,\n        config.cerebrasApiKeySecret.secretArn,\n        config.stripeSecretKeySecret.secretArn,\n        config.stripeWebhookSecret.secretArn,\n      ],\n    }),\n  )\n\n  const taskDefinition = new FargateTaskDefinition(scope, 'ItoTaskDefinition', {\n    taskRole: fargateTaskRole,\n    cpu: isDev(config.stageName) ? 1024 : 4096,\n    memoryLimitMiB: isDev(config.stageName) ? 2048 : 8192,\n    runtimePlatform: {\n      operatingSystemFamily: OperatingSystemFamily.LINUX,\n      cpuArchitecture: CpuArchitecture.ARM64,\n    },\n    executionRole: taskExecutionRole,\n  })\n\n  const containerName = 'ItoServerContainer'\n\n  taskDefinition.addContainer(containerName, {\n    image: ContainerImage.fromEcrRepository(config.serviceRepo, 'latest'),\n    portMappings: [{ containerPort: 3000 }],\n    secrets: {\n      DB_USER: EcsSecret.fromSecretsManager(\n        config.dbCredentialsSecret,\n        'username',\n      ),\n      DB_PASS: EcsSecret.fromSecretsManager(\n        config.dbCredentialsSecret,\n        'password',\n      ),\n      GROQ_API_KEY: EcsSecret.fromSecretsManager(config.groqApiKeySecret),\n      CEREBRAS_API_KEY: EcsSecret.fromSecretsManager(\n        config.cerebrasApiKeySecret,\n      ),\n      STRIPE_SECRET_KEY: EcsSecret.fromSecretsManager(\n        config.stripeSecretKeySecret,\n      ),\n      STRIPE_WEBHOOK_SECRET: EcsSecret.fromSecretsManager(\n        config.stripeWebhookSecret,\n      ),\n    },\n    environment: {\n      DB_HOST: config.dbEndpoint,\n      DB_NAME: config.dbName,\n      DB_PORT: config.dbPort.toString(),\n      REQUIRE_AUTH: 'true',\n      AUTH0_DOMAIN: process.env.AUTH0_DOMAIN || '',\n      AUTH0_AUDIENCE: process.env.AUTH0_AUDIENCE || '',\n      AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID || '',\n      AUTH0_MGMT_CLIENT_ID: process.env.AUTH0_MGMT_CLIENT_ID || '',\n      AUTH0_MGMT_CLIENT_SECRET: process.env.AUTH0_MGMT_CLIENT_SECRET || '',\n      AUTH0_CALLBACK_URL: `https://${config.domainName}/callback`,\n      STRIPE_PRICE_ID: process.env.STRIPE_PRICE_ID || '',\n      APP_PROTOCOL: process.env.APP_PROTOCOL || '',\n      STRIPE_PUBLIC_BASE_URL: process.env.STRIPE_PUBLIC_BASE_URL || '',\n      GROQ_TRANSCRIPTION_MODEL: 'whisper-large-v3',\n      CLIENT_LOG_GROUP_NAME: config.clientLogGroup.logGroupName,\n      ...(config.blobStorageBucketName && {\n        BLOB_STORAGE_BUCKET: config.blobStorageBucketName,\n      }),\n      ...(config.timingBucketName && {\n        TIMING_BUCKET: config.timingBucketName,\n      }),\n      ITO_ENV: config.stageName,\n      SHOW_ALL_REQUEST_LOGS: 'true',\n    },\n    logging: new AwsLogDriver({\n      streamPrefix: 'ito-server',\n      logGroup: config.serverLogGroup,\n    }),\n  })\n\n  const taskLogsPolicy = new Policy(scope, 'ItoTaskLogsPolicy', {\n    statements: [\n      new PolicyStatement({\n        actions: [\n          'logs:CreateLogGroup',\n          'logs:CreateLogStream',\n          'logs:PutLogEvents',\n          'logs:DescribeLogStreams',\n          'logs:DescribeLogGroups',\n        ],\n        resources: [\n          `arn:aws:logs:${stack.region}:${stack.account}:log-group:/ito/${config.stageName}/client`,\n          `arn:aws:logs:${stack.region}:${stack.account}:log-group:/ito/${config.stageName}/client:log-stream:*`,\n          `arn:aws:logs:${stack.region}:${stack.account}:log-group:/ito/${config.stageName}/server`,\n          `arn:aws:logs:${stack.region}:${stack.account}:log-group:/ito/${config.stageName}/server:log-stream:*`,\n        ],\n      }),\n    ],\n  })\n\n  fargateTaskRole.attachInlinePolicy(taskLogsPolicy)\n\n  return {\n    taskDefinition,\n    taskRole: fargateTaskRole,\n    taskExecutionRole,\n    containerName,\n    taskLogsPolicy,\n  }\n}\n"
  },
  {
    "path": "server/infra/lib/service/firehose-config.ts",
    "content": "import { Construct } from 'constructs'\nimport { Stack, Duration } from 'aws-cdk-lib'\nimport { CfnSubscriptionFilter, ILogGroup } from 'aws-cdk-lib/aws-logs'\nimport { CfnDeliveryStream } from 'aws-cdk-lib/aws-kinesisfirehose'\nimport { Bucket } from 'aws-cdk-lib/aws-s3'\nimport { Domain } from 'aws-cdk-lib/aws-opensearchservice'\nimport {\n  Role as IamRole,\n  IRole,\n  ServicePrincipal,\n  Policy,\n  PolicyStatement,\n} from 'aws-cdk-lib/aws-iam'\nimport { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'\nimport * as cr from 'aws-cdk-lib/custom-resources'\n\nexport interface FirehoseConfig {\n  stageName: string\n  opensearchDomain: Domain\n  firehoseBackupBucket: Bucket\n  firehoseRole: IRole\n  clientLogGroup: ILogGroup\n  serverLogGroup: ILogGroup\n  ensureClientLogGroup: cr.AwsCustomResource\n  ensureServerLogGroup: cr.AwsCustomResource\n}\n\nexport interface FirehoseResources {\n  clientDeliveryStream: CfnDeliveryStream\n  serverDeliveryStream: CfnDeliveryStream\n  clientProcessor: NodejsFunction\n  serverProcessor: NodejsFunction\n  logsToFirehoseRole: IamRole\n  logsToFirehosePolicy: Policy\n  clientSubscription: CfnSubscriptionFilter\n  serverSubscription: CfnSubscriptionFilter\n}\n\nexport function createFirehoseStreams(\n  scope: Construct,\n  config: FirehoseConfig,\n): FirehoseResources {\n  Stack.of(scope)\n\n  const clientProcessor = new NodejsFunction(\n    scope,\n    'ItoClientFirehoseProcessor',\n    {\n      entry: 'lambdas/firehose-transform.ts',\n      handler: 'handler',\n      environment: { DATASET: 'client', STAGE: config.stageName },\n      timeout: Duration.seconds(30),\n    },\n  )\n\n  const serverProcessor = new NodejsFunction(\n    scope,\n    'ItoServerFirehoseProcessor',\n    {\n      entry: 'lambdas/firehose-transform.ts',\n      handler: 'handler',\n      environment: { DATASET: 'server', STAGE: config.stageName },\n      timeout: Duration.seconds(30),\n    },\n  )\n\n  const clientInvokeGrant = clientProcessor.grantInvoke(config.firehoseRole)\n  const serverInvokeGrant = serverProcessor.grantInvoke(config.firehoseRole)\n\n  const clientDeliveryStream = new CfnDeliveryStream(\n    scope,\n    'ItoClientLogsToOs-client',\n    {\n      deliveryStreamName: `${config.stageName}-ito-client-logs`,\n      amazonopensearchserviceDestinationConfiguration: {\n        domainArn: config.opensearchDomain.domainArn,\n        indexName: 'client-logs',\n        indexRotationPeriod: 'OneDay',\n        roleArn: config.firehoseRole.roleArn,\n        bufferingHints: { intervalInSeconds: 60, sizeInMBs: 5 },\n        s3BackupMode: 'AllDocuments',\n        s3Configuration: {\n          bucketArn: config.firehoseBackupBucket.bucketArn,\n          roleArn: config.firehoseRole.roleArn,\n          bufferingHints: { intervalInSeconds: 60, sizeInMBs: 5 },\n          compressionFormat: 'GZIP',\n        },\n        processingConfiguration: {\n          enabled: true,\n          processors: [\n            {\n              type: 'Lambda',\n              parameters: [\n                {\n                  parameterName: 'LambdaArn',\n                  parameterValue: clientProcessor.functionArn,\n                },\n                { parameterName: 'NumberOfRetries', parameterValue: '3' },\n                {\n                  parameterName: 'BufferIntervalInSeconds',\n                  parameterValue: '60',\n                },\n                { parameterName: 'BufferSizeInMBs', parameterValue: '3' },\n              ],\n            },\n          ],\n        },\n      },\n    },\n  )\n\n  const serverDeliveryStream = new CfnDeliveryStream(\n    scope,\n    'ItoServerLogsToOs-server',\n    {\n      deliveryStreamName: `${config.stageName}-ito-server-logs`,\n      amazonopensearchserviceDestinationConfiguration: {\n        domainArn: config.opensearchDomain.domainArn,\n        indexName: 'server-logs',\n        indexRotationPeriod: 'OneDay',\n        roleArn: config.firehoseRole.roleArn,\n        bufferingHints: { intervalInSeconds: 60, sizeInMBs: 5 },\n        s3BackupMode: 'AllDocuments',\n        s3Configuration: {\n          bucketArn: config.firehoseBackupBucket.bucketArn,\n          roleArn: config.firehoseRole.roleArn,\n          bufferingHints: { intervalInSeconds: 60, sizeInMBs: 5 },\n          compressionFormat: 'GZIP',\n        },\n        processingConfiguration: {\n          enabled: true,\n          processors: [\n            {\n              type: 'Lambda',\n              parameters: [\n                {\n                  parameterName: 'LambdaArn',\n                  parameterValue: serverProcessor.functionArn,\n                },\n                { parameterName: 'NumberOfRetries', parameterValue: '3' },\n                {\n                  parameterName: 'BufferIntervalInSeconds',\n                  parameterValue: '60',\n                },\n                { parameterName: 'BufferSizeInMBs', parameterValue: '3' },\n              ],\n            },\n          ],\n        },\n      },\n    },\n  )\n\n  const clientS3PolicyAttach = config.firehoseRole.addToPrincipalPolicy(\n    new PolicyStatement({\n      actions: [\n        's3:AbortMultipartUpload',\n        's3:GetBucketLocation',\n        's3:GetObject',\n        's3:ListBucket',\n        's3:ListBucketMultipartUploads',\n        's3:PutObject',\n      ],\n      resources: [\n        config.firehoseBackupBucket.bucketArn,\n        `${config.firehoseBackupBucket.bucketArn}/*`,\n      ],\n    }),\n  )\n\n  const clientEsPolicyAttach = config.firehoseRole.addToPrincipalPolicy(\n    new PolicyStatement({\n      actions: ['es:*'],\n      resources: [\n        config.opensearchDomain.domainArn,\n        `${config.opensearchDomain.domainArn}/*`,\n      ],\n    }),\n  )\n\n  const clientEsDescribePolicyAttach = config.firehoseRole.addToPrincipalPolicy(\n    new PolicyStatement({\n      actions: [\n        'es:DescribeElasticsearchDomain',\n        'es:DescribeElasticsearchDomains',\n        'es:DescribeElasticsearchDomainConfig',\n        'es:DescribeDomain',\n        'es:DescribeDomains',\n        'es:DescribeDomainConfig',\n      ],\n      resources: ['*'],\n    }),\n  )\n\n  if (clientS3PolicyAttach.policyDependable)\n    clientDeliveryStream.node.addDependency(\n      clientS3PolicyAttach.policyDependable,\n    )\n  if (clientEsPolicyAttach.policyDependable)\n    clientDeliveryStream.node.addDependency(\n      clientEsPolicyAttach.policyDependable,\n    )\n  if (clientEsDescribePolicyAttach.policyDependable)\n    clientDeliveryStream.node.addDependency(\n      clientEsDescribePolicyAttach.policyDependable,\n    )\n  clientInvokeGrant.applyBefore(clientDeliveryStream)\n\n  const serverS3PolicyAttach = config.firehoseRole.addToPrincipalPolicy(\n    new PolicyStatement({\n      actions: [\n        's3:AbortMultipartUpload',\n        's3:GetBucketLocation',\n        's3:GetObject',\n        's3:ListBucket',\n        's3:ListBucketMultipartUploads',\n        's3:PutObject',\n      ],\n      resources: [\n        config.firehoseBackupBucket.bucketArn,\n        `${config.firehoseBackupBucket.bucketArn}/*`,\n      ],\n    }),\n  )\n\n  const serverEsPolicyAttach = config.firehoseRole.addToPrincipalPolicy(\n    new PolicyStatement({\n      actions: ['es:*'],\n      resources: [\n        config.opensearchDomain.domainArn,\n        `${config.opensearchDomain.domainArn}/*`,\n      ],\n    }),\n  )\n\n  const serverEsDescribePolicyAttach = config.firehoseRole.addToPrincipalPolicy(\n    new PolicyStatement({\n      actions: [\n        'es:DescribeElasticsearchDomain',\n        'es:DescribeElasticsearchDomains',\n        'es:DescribeElasticsearchDomainConfig',\n        'es:DescribeDomain',\n        'es:DescribeDomains',\n        'es:DescribeDomainConfig',\n      ],\n      resources: ['*'],\n    }),\n  )\n\n  if (serverS3PolicyAttach.policyDependable)\n    serverDeliveryStream.node.addDependency(\n      serverS3PolicyAttach.policyDependable,\n    )\n  if (serverEsPolicyAttach.policyDependable)\n    serverDeliveryStream.node.addDependency(\n      serverEsPolicyAttach.policyDependable,\n    )\n  if (serverEsDescribePolicyAttach.policyDependable)\n    serverDeliveryStream.node.addDependency(\n      serverEsDescribePolicyAttach.policyDependable,\n    )\n  serverInvokeGrant.applyBefore(serverDeliveryStream)\n\n  const logsToFirehoseRole = new IamRole(scope, 'ItoLogsToFirehoseRole', {\n    assumedBy: new ServicePrincipal('logs.amazonaws.com'),\n  })\n\n  const logsToFirehosePolicy = new Policy(\n    scope,\n    'ItoLogsToFirehoseWritePolicy',\n    {\n      statements: [\n        new PolicyStatement({\n          actions: ['firehose:PutRecord', 'firehose:PutRecordBatch'],\n          resources: [\n            clientDeliveryStream.attrArn,\n            serverDeliveryStream.attrArn,\n          ],\n        }),\n      ],\n    },\n  )\n\n  logsToFirehoseRole.attachInlinePolicy(logsToFirehosePolicy)\n\n  const clientSubscription = new CfnSubscriptionFilter(\n    scope,\n    'ItoClientLogsSubscription',\n    {\n      logGroupName: config.clientLogGroup.logGroupName,\n      destinationArn: clientDeliveryStream.attrArn,\n      filterPattern: '',\n      roleArn: logsToFirehoseRole.roleArn,\n    },\n  )\n\n  clientSubscription.addDependency(clientDeliveryStream)\n  clientSubscription.node.addDependency(logsToFirehosePolicy)\n  clientSubscription.node.addDependency(config.ensureClientLogGroup)\n\n  const serverSubscription = new CfnSubscriptionFilter(\n    scope,\n    'ItoServerLogsSubscription',\n    {\n      logGroupName: config.serverLogGroup.logGroupName,\n      destinationArn: serverDeliveryStream.attrArn,\n      filterPattern: '',\n      roleArn: logsToFirehoseRole.roleArn,\n    },\n  )\n\n  serverSubscription.addDependency(serverDeliveryStream)\n  serverSubscription.node.addDependency(logsToFirehosePolicy)\n  serverSubscription.node.addDependency(config.ensureServerLogGroup)\n\n  return {\n    clientDeliveryStream,\n    serverDeliveryStream,\n    clientProcessor,\n    serverProcessor,\n    logsToFirehoseRole,\n    logsToFirehosePolicy,\n    clientSubscription,\n    serverSubscription,\n  }\n}\n"
  },
  {
    "path": "server/infra/lib/service/index.ts",
    "content": "export * from './log-groups'\nexport * from './fargate-task'\nexport * from './firehose-config'\nexport * from './opensearch-bootstrap'\nexport * from './migration-lambda'\n"
  },
  {
    "path": "server/infra/lib/service/log-groups.ts",
    "content": "import { Construct } from 'constructs'\nimport * as cr from 'aws-cdk-lib/custom-resources'\nimport { PolicyStatement } from 'aws-cdk-lib/aws-iam'\nimport { ILogGroup, LogGroup } from 'aws-cdk-lib/aws-logs'\n\nexport interface LogGroupConfig {\n  stageName: string\n}\n\nexport interface LogGroupResources {\n  clientLogGroup: ILogGroup\n  serverLogGroup: ILogGroup\n  ensureClientLogGroup: cr.AwsCustomResource\n  ensureServerLogGroup: cr.AwsCustomResource\n}\n\nexport function createLogGroups(\n  scope: Construct,\n  config: LogGroupConfig,\n): LogGroupResources {\n  const ensureClientLogGroup = new cr.AwsCustomResource(\n    scope,\n    'EnsureClientLogGroup',\n    {\n      onCreate: {\n        service: 'CloudWatchLogs',\n        action: 'createLogGroup',\n        parameters: { logGroupName: `/ito/${config.stageName}/client` },\n        physicalResourceId: cr.PhysicalResourceId.of(\n          `loggroup-${config.stageName}-client`,\n        ),\n        ignoreErrorCodesMatching: 'ResourceAlreadyExistsException',\n      },\n      policy: cr.AwsCustomResourcePolicy.fromStatements([\n        new PolicyStatement({\n          actions: ['logs:CreateLogGroup'],\n          resources: ['*'],\n        }),\n      ]),\n    },\n  )\n\n  const ensureServerLogGroup = new cr.AwsCustomResource(\n    scope,\n    'EnsureServerLogGroup',\n    {\n      onCreate: {\n        service: 'CloudWatchLogs',\n        action: 'createLogGroup',\n        parameters: { logGroupName: `/ito/${config.stageName}/server` },\n        physicalResourceId: cr.PhysicalResourceId.of(\n          `loggroup-${config.stageName}-server`,\n        ),\n        ignoreErrorCodesMatching: 'ResourceAlreadyExistsException',\n      },\n      policy: cr.AwsCustomResourcePolicy.fromStatements([\n        new PolicyStatement({\n          actions: ['logs:CreateLogGroup'],\n          resources: ['*'],\n        }),\n      ]),\n    },\n  )\n\n  const clientLogGroup = LogGroup.fromLogGroupName(\n    scope,\n    'ItoClientLogsGroup',\n    `/ito/${config.stageName}/client`,\n  )\n\n  const serverLogGroup = LogGroup.fromLogGroupName(\n    scope,\n    'ItoServerLogsGroup',\n    `/ito/${config.stageName}/server`,\n  )\n\n  return {\n    clientLogGroup,\n    serverLogGroup,\n    ensureClientLogGroup,\n    ensureServerLogGroup,\n  }\n}\n"
  },
  {
    "path": "server/infra/lib/service/migration-lambda.ts",
    "content": "import { Construct } from 'constructs'\nimport { Duration } from 'aws-cdk-lib'\nimport { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'\nimport { PolicyStatement } from 'aws-cdk-lib/aws-iam'\nimport { LogGroup } from 'aws-cdk-lib/aws-logs'\nimport * as cr from 'aws-cdk-lib/custom-resources'\nimport {\n  FargateTaskDefinition,\n  FargateService,\n  Cluster,\n} from 'aws-cdk-lib/aws-ecs'\nimport { Vpc } from 'aws-cdk-lib/aws-ec2'\nimport { IRole } from 'aws-cdk-lib/aws-iam'\n\nexport interface MigrationLambdaConfig {\n  stageName: string\n  dbName: string\n  cluster: Cluster\n  taskDefinition: FargateTaskDefinition\n  vpc: Vpc\n  fargateService: FargateService\n  containerName: string\n  taskExecutionRole: IRole\n  taskRole: IRole\n}\n\nexport interface MigrationLambdaResources {\n  migrationLambda: NodejsFunction\n}\n\nexport function createMigrationLambda(\n  scope: Construct,\n  config: MigrationLambdaConfig,\n): MigrationLambdaResources {\n  const logGroupName = `/aws/lambda/${config.stageName}-${config.dbName}-migration`\n\n  // Ensure log group exists (handles case where it already exists)\n  const ensureLogGroup = new cr.AwsCustomResource(\n    scope,\n    'EnsureMigrationLogGroup',\n    {\n      onCreate: {\n        service: 'CloudWatchLogs',\n        action: 'createLogGroup',\n        parameters: { logGroupName },\n        physicalResourceId: cr.PhysicalResourceId.of(\n          `loggroup-${config.stageName}-migration`,\n        ),\n        ignoreErrorCodesMatching: 'ResourceAlreadyExistsException',\n      },\n      onUpdate: {\n        service: 'CloudWatchLogs',\n        action: 'createLogGroup',\n        parameters: { logGroupName },\n        physicalResourceId: cr.PhysicalResourceId.of(\n          `loggroup-${config.stageName}-migration`,\n        ),\n        ignoreErrorCodesMatching: 'ResourceAlreadyExistsException',\n      },\n      policy: cr.AwsCustomResourcePolicy.fromStatements([\n        new PolicyStatement({\n          actions: ['logs:CreateLogGroup'],\n          resources: ['*'],\n        }),\n      ]),\n    },\n  )\n\n  const logGroup = LogGroup.fromLogGroupName(\n    scope,\n    'MigrationLambdaLogGroup',\n    logGroupName,\n  )\n\n  const migrationLambda = new NodejsFunction(scope, 'ItoMigrationLambda', {\n    functionName: `${config.stageName}-${config.dbName}-migration`,\n    entry: 'lambdas/run-migration.ts',\n    handler: 'handler',\n    logGroup,\n    environment: {\n      CLUSTER: config.cluster.clusterName,\n      TASK_DEF: config.taskDefinition.taskDefinitionArn,\n      SUBNETS: config.vpc.privateSubnets.map(s => s.subnetId).join(','),\n      SECURITY_GROUPS:\n        config.fargateService.connections.securityGroups[0].securityGroupId,\n      STAGE_NAME: config.stageName,\n      CONTAINER_NAME: config.containerName,\n    },\n    timeout: Duration.minutes(10),\n  })\n\n  // Ensure log group is created before Lambda\n  migrationLambda.node.addDependency(ensureLogGroup)\n\n  migrationLambda.addToRolePolicy(\n    new PolicyStatement({\n      actions: ['ecs:RunTask'],\n      resources: [config.taskDefinition.taskDefinitionArn],\n    }),\n  )\n\n  migrationLambda.addToRolePolicy(\n    new PolicyStatement({\n      actions: ['ecs:DescribeTasks'],\n      resources: ['*'],\n    }),\n  )\n\n  migrationLambda.addToRolePolicy(\n    new PolicyStatement({\n      actions: ['iam:PassRole'],\n      resources: [config.taskExecutionRole.roleArn, config.taskRole.roleArn],\n      conditions: {\n        StringEquals: {\n          'iam:PassedToService': 'ecs-tasks.amazonaws.com',\n        },\n      },\n    }),\n  )\n\n  return {\n    migrationLambda,\n  }\n}\n"
  },
  {
    "path": "server/infra/lib/service/opensearch-bootstrap.ts",
    "content": "import { Construct } from 'constructs'\nimport { Stack, Duration, CustomResource } from 'aws-cdk-lib'\nimport { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'\nimport { PolicyStatement } from 'aws-cdk-lib/aws-iam'\nimport { Domain } from 'aws-cdk-lib/aws-opensearchservice'\nimport * as cr from 'aws-cdk-lib/custom-resources'\n\nexport interface OpenSearchBootstrapConfig {\n  stageName: string\n  opensearchDomain: Domain\n}\n\nexport interface OpenSearchBootstrapResources {\n  bootstrapLambda: NodejsFunction\n  bootstrapProvider: cr.Provider\n  bootstrapResource: CustomResource\n}\n\nexport function createOpenSearchBootstrap(\n  scope: Construct,\n  config: OpenSearchBootstrapConfig,\n): OpenSearchBootstrapResources {\n  const stack = Stack.of(scope)\n\n  const bootstrapLambda = new NodejsFunction(scope, 'ItoOpenSearchBootstrap', {\n    entry: 'lambdas/opensearch-bootstrap.ts',\n    handler: 'handler',\n    environment: {\n      DOMAIN_ENDPOINT: config.opensearchDomain.domainEndpoint,\n      REGION: stack.region,\n      STAGE: config.stageName,\n    },\n    timeout: Duration.minutes(2),\n  })\n\n  bootstrapLambda.addToRolePolicy(\n    new PolicyStatement({\n      actions: ['es:ESHttpGet', 'es:ESHttpPut'],\n      resources: [\n        config.opensearchDomain.domainArn,\n        `${config.opensearchDomain.domainArn}/*`,\n      ],\n    }),\n  )\n\n  const bootstrapProvider = new cr.Provider(\n    scope,\n    'ItoOpenSearchBootstrapProvider',\n    {\n      onEventHandler: bootstrapLambda,\n    },\n  )\n\n  const bootstrapResource = new CustomResource(\n    scope,\n    'ItoOpenSearchBootstrapResource',\n    {\n      serviceToken: bootstrapProvider.serviceToken,\n      properties: {\n        domain: config.opensearchDomain.domainEndpoint,\n        stage: config.stageName,\n      },\n    },\n  )\n\n  return {\n    bootstrapLambda,\n    bootstrapProvider,\n    bootstrapResource,\n  }\n}\n"
  },
  {
    "path": "server/infra/lib/service-stack.ts",
    "content": "import {\n  CfnOutput,\n  Duration,\n  RemovalPolicy,\n  Stack,\n  StackProps,\n  Stage,\n  Tags,\n} from 'aws-cdk-lib'\nimport { Construct } from 'constructs'\nimport {\n  Certificate,\n  CertificateValidation,\n} from 'aws-cdk-lib/aws-certificatemanager'\nimport {\n  ApplicationProtocol,\n  Protocol,\n  SslPolicy,\n} from 'aws-cdk-lib/aws-elasticloadbalancingv2'\nimport { BlockPublicAccess, Bucket, IBucket } from 'aws-cdk-lib/aws-s3'\nimport { CLUSTER_NAME, DB_NAME, DB_PORT, SERVICE_NAME } from './constants'\nimport { Cluster, FargateService } from 'aws-cdk-lib/aws-ecs'\nimport { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns'\nimport { Secret } from 'aws-cdk-lib/aws-secretsmanager'\nimport { Vpc } from 'aws-cdk-lib/aws-ec2'\nimport { Repository } from 'aws-cdk-lib/aws-ecr'\nimport { HostedZone } from 'aws-cdk-lib/aws-route53'\nimport { AppStage } from '../bin/infra'\nimport { isDev } from './helpers'\nimport { Role as IamRole, PolicyStatement } from 'aws-cdk-lib/aws-iam'\nimport { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'\nimport { Domain } from 'aws-cdk-lib/aws-opensearchservice'\n\n// Import our new modules\nimport { createLogGroups } from './service/log-groups'\nimport { createFargateTask } from './service/fargate-task'\nimport { createFirehoseStreams } from './service/firehose-config'\nimport { createOpenSearchBootstrap } from './service/opensearch-bootstrap'\nimport { createMigrationLambda } from './service/migration-lambda'\n\nexport interface ServiceStackProps extends StackProps {\n  dbSecretArn: string\n  dbEndpoint: string\n  serviceRepo: Repository\n  vpc: Vpc\n  opensearchDomain: Domain\n  blobStorageBucket: IBucket\n  timingBucketName: string\n}\n\nexport class ServiceStack extends Stack {\n  public readonly fargateService: FargateService\n  public readonly migrationLambda: NodejsFunction\n  public readonly albFargate: ApplicationLoadBalancedFargateService\n\n  constructor(scope: Construct, id: string, props: ServiceStackProps) {\n    super(scope, id, props)\n\n    const stage = Stage.of(this) as AppStage\n    const stageName = stage.stageName\n\n    // Import secrets\n    const dbCredentialsSecret = Secret.fromSecretCompleteArn(\n      this,\n      'ImportedDbSecret',\n      props.dbSecretArn,\n    )\n\n    const groqApiKeySecret = Secret.fromSecretNameV2(\n      this,\n      'GroqApiKey',\n      `${stageName}/ito/groq-api-key`,\n    )\n\n    const cerebrasApiKeySecret = Secret.fromSecretNameV2(\n      this,\n      'CerebrasApiKey',\n      `${stageName}/ito/cerebras-api-key`,\n    )\n\n    const stripeSecretKeySecret = Secret.fromSecretNameV2(\n      this,\n      'StripeSecretKey',\n      `${stageName}/ito/stripe-secret-key`,\n    )\n\n    const stripeWebhookSecret = Secret.fromSecretNameV2(\n      this,\n      'StripeWebhookSecret',\n      `${stageName}/ito/stripe-webhook`,\n    )\n\n    // Setup domain and certificate\n    const zone = HostedZone.fromLookup(this, 'HostedZone', {\n      domainName: 'ito-api.com',\n    })\n\n    const domainName = `${stageName}.ito-api.com`\n    const cert = new Certificate(this, 'SiteCert', {\n      domainName,\n      validation: CertificateValidation.fromDns(zone),\n    })\n\n    // Create log groups\n    const logGroupResources = createLogGroups(this, { stageName })\n\n    // Import timing bucket from platform stack\n    const timingBucket = Bucket.fromBucketName(\n      this,\n      'TimingBucket',\n      props.timingBucketName,\n    )\n\n    // Create Fargate task\n    const fargateTaskResources = createFargateTask(this, {\n      stageName,\n      serviceRepo: props.serviceRepo,\n      dbCredentialsSecret,\n      groqApiKeySecret,\n      cerebrasApiKeySecret,\n      stripeSecretKeySecret,\n      stripeWebhookSecret: stripeWebhookSecret,\n      dbEndpoint: props.dbEndpoint,\n      dbName: DB_NAME,\n      dbPort: DB_PORT,\n      domainName,\n      clientLogGroup: logGroupResources.clientLogGroup,\n      serverLogGroup: logGroupResources.serverLogGroup,\n      blobStorageBucketName: props.blobStorageBucket.bucketName,\n      timingBucketName: props.timingBucketName,\n    })\n\n    // Grant Fargate task permissions to access blob storage\n    props.blobStorageBucket.grantReadWrite(fargateTaskResources.taskRole)\n    props.blobStorageBucket.grantDelete(fargateTaskResources.taskRole)\n\n    // Grant Fargate task permissions to write timing data to S3\n    timingBucket.grantPut(fargateTaskResources.taskRole)\n\n    // Create ECS cluster\n    const cluster = new Cluster(this, 'ItoEcsCluster', {\n      vpc: props.vpc,\n      clusterName: `${stageName}-${CLUSTER_NAME}`,\n    })\n\n    // Create S3 buckets\n    const logBucket = new Bucket(this, 'ItoAlbLogsBucket', {\n      bucketName: `${stageName}-${this.account}-${this.region}-ito-alb-logs`,\n      removalPolicy: isDev(stageName)\n        ? RemovalPolicy.DESTROY\n        : RemovalPolicy.RETAIN,\n      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,\n      enforceSSL: true,\n      versioned: true,\n    })\n\n    const firehoseBackupBucket = new Bucket(this, 'ItoFirehoseBackupBucket', {\n      bucketName: `${stageName}-${this.account}-${this.region}-ito-firehose-bucket`,\n      removalPolicy: isDev(stageName)\n        ? RemovalPolicy.DESTROY\n        : RemovalPolicy.RETAIN,\n      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,\n      enforceSSL: true,\n      versioned: true,\n    })\n\n    // Create Fargate service\n    const fargateService = new ApplicationLoadBalancedFargateService(\n      this,\n      'ItoFargateService',\n      {\n        cluster,\n        serviceName: `${stageName}-${SERVICE_NAME}`,\n        desiredCount: isDev(stageName) ? 1 : 2,\n        publicLoadBalancer: true,\n        taskDefinition: fargateTaskResources.taskDefinition,\n        protocol: ApplicationProtocol.HTTPS,\n        domainZone: zone,\n        domainName,\n        certificate: cert,\n        redirectHTTP: true,\n        sslPolicy: SslPolicy.RECOMMENDED,\n        circuitBreaker: { enable: true, rollback: true },\n      },\n    )\n\n    fargateService.targetGroup.configureHealthCheck({\n      protocol: Protocol.HTTP,\n      path: '/',\n      interval: Duration.seconds(30),\n      timeout: Duration.seconds(5),\n      healthyThresholdCount: 2,\n      unhealthyThresholdCount: 5,\n    })\n\n    const scalableTarget = fargateService.service.autoScaleTaskCount({\n      minCapacity: 1,\n      maxCapacity: 5,\n    })\n\n    scalableTarget.scaleOnCpuUtilization('ItoServerCpuScalingPolicy', {\n      targetUtilizationPercent: 65,\n    })\n\n    // Create migration Lambda\n    const migrationLambdaResources = createMigrationLambda(this, {\n      stageName,\n      dbName: DB_NAME,\n      cluster,\n      taskDefinition: fargateTaskResources.taskDefinition,\n      vpc: props.vpc,\n      fargateService: fargateService.service,\n      containerName: fargateTaskResources.containerName,\n      taskExecutionRole: fargateTaskResources.taskExecutionRole,\n      taskRole: fargateTaskResources.taskRole,\n    })\n\n    // Configure ALB logging\n    const alb = fargateService.loadBalancer\n    alb.logAccessLogs(logBucket, 'ito-alb-access-logs')\n\n    // Ensure ECS Service waits for inline policy attachment and log groups creation\n    fargateService.service.node.addDependency(\n      fargateTaskResources.taskLogsPolicy,\n    )\n    fargateService.service.node.addDependency(\n      logGroupResources.ensureClientLogGroup,\n    )\n    fargateService.service.node.addDependency(\n      logGroupResources.ensureServerLogGroup,\n    )\n\n    // Import Firehose role created in platform stack\n    const firehoseRole = IamRole.fromRoleArn(\n      this,\n      'ItoFirehoseRoleImported',\n      `arn:aws:iam::${this.account}:role/${stageName}-ItoFirehoseRole`,\n      { mutable: true },\n    )\n\n    // Add Firehose role policies\n    firehoseRole.addToPrincipalPolicy(\n      new PolicyStatement({\n        actions: [\n          's3:AbortMultipartUpload',\n          's3:GetBucketLocation',\n          's3:GetObject',\n          's3:ListBucket',\n          's3:ListBucketMultipartUploads',\n          's3:PutObject',\n        ],\n        resources: [\n          firehoseBackupBucket.bucketArn,\n          `${firehoseBackupBucket.bucketArn}/*`,\n        ],\n      }),\n    )\n\n    firehoseRole.addToPrincipalPolicy(\n      new PolicyStatement({\n        actions: ['es:*'],\n        resources: [\n          props.opensearchDomain.domainArn,\n          `${props.opensearchDomain.domainArn}/*`,\n        ],\n      }),\n    )\n\n    firehoseRole.addToPrincipalPolicy(\n      new PolicyStatement({\n        actions: [\n          'es:DescribeElasticsearchDomain',\n          'es:DescribeElasticsearchDomains',\n          'es:DescribeElasticsearchDomainConfig',\n          'es:DescribeDomain',\n          'es:DescribeDomains',\n          'es:DescribeDomainConfig',\n        ],\n        resources: ['*'],\n      }),\n    )\n\n    // Create Firehose delivery streams\n    createFirehoseStreams(this, {\n      stageName,\n      opensearchDomain: props.opensearchDomain,\n      firehoseBackupBucket,\n      firehoseRole,\n      clientLogGroup: logGroupResources.clientLogGroup,\n      serverLogGroup: logGroupResources.serverLogGroup,\n      ensureClientLogGroup: logGroupResources.ensureClientLogGroup,\n      ensureServerLogGroup: logGroupResources.ensureServerLogGroup,\n    })\n\n    // Create OpenSearch bootstrap\n    createOpenSearchBootstrap(this, {\n      stageName,\n      opensearchDomain: props.opensearchDomain,\n    })\n\n    // Set stack properties\n    this.fargateService = fargateService.service\n    this.albFargate = fargateService\n    this.migrationLambda = migrationLambdaResources.migrationLambda\n\n    // Outputs\n    new CfnOutput(this, 'ServiceURL', {\n      value: fargateService.loadBalancer.loadBalancerDnsName,\n    })\n\n    // Tags\n    Tags.of(this).add('Project', 'Ito')\n  }\n}\n"
  },
  {
    "path": "server/infra/lib/timing-config.ts",
    "content": "import { Construct } from 'constructs'\nimport { Duration, RemovalPolicy } from 'aws-cdk-lib'\nimport { Bucket, BlockPublicAccess, EventType } from 'aws-cdk-lib/aws-s3'\nimport { Domain } from 'aws-cdk-lib/aws-opensearchservice'\nimport { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'\nimport { SqsDestination } from 'aws-cdk-lib/aws-s3-notifications'\nimport { PolicyStatement } from 'aws-cdk-lib/aws-iam'\nimport { Queue } from 'aws-cdk-lib/aws-sqs'\nimport { SqsEventSource } from 'aws-cdk-lib/aws-lambda-event-sources'\n\nexport interface TimingConfig {\n  stageName: string\n  opensearchDomain: Domain\n  accountId: string\n  region: string\n}\n\nexport interface TimingResources {\n  timingBucket: Bucket\n  timingMergerLambda: NodejsFunction\n  timingQueue: Queue\n  timingDLQ: Queue\n}\n\nexport function createTimingInfrastructure(\n  scope: Construct,\n  config: TimingConfig,\n): TimingResources {\n  const isDev = config.stageName === 'dev'\n\n  // Create S3 bucket for raw timing data\n  const timingBucket = new Bucket(scope, 'ItoTimingDataBucket', {\n    bucketName: `${config.stageName}-${config.accountId}-${config.region}-ito-timing-data`,\n    removalPolicy: isDev ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN,\n    blockPublicAccess: BlockPublicAccess.BLOCK_ALL,\n    enforceSSL: true,\n    versioned: false, // Don't need versioning for timing data\n    lifecycleRules: [\n      {\n        // Auto-delete raw timing data after 7 days (it's in OpenSearch by then)\n        expiration: Duration.days(7),\n        enabled: true,\n      },\n    ],\n  })\n\n  // Create DLQ for failed timing events\n  const timingDLQ = new Queue(scope, 'ItoTimingDLQ', {\n    queueName: `${config.stageName}-ito-timing-dlq`,\n    retentionPeriod: Duration.days(14), // Keep failed messages for 14 days\n    removalPolicy: isDev ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN,\n  })\n\n  // Create main queue for timing events with DLQ\n  const timingQueue = new Queue(scope, 'ItoTimingQueue', {\n    queueName: `${config.stageName}-ito-timing-queue`,\n    visibilityTimeout: Duration.seconds(60), // Should be >= Lambda timeout\n    retentionPeriod: Duration.days(7),\n    deadLetterQueue: {\n      queue: timingDLQ,\n      maxReceiveCount: 3, // Retry failed messages 3 times before sending to DLQ\n    },\n    removalPolicy: isDev ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN,\n  })\n\n  // Create timing merger Lambda\n  const timingMergerLambda = new NodejsFunction(scope, 'ItoTimingMerger', {\n    entry: 'lambdas/timing-merger.ts',\n    handler: 'handler',\n    environment: {\n      OPENSEARCH_ENDPOINT: config.opensearchDomain.domainEndpoint,\n      STAGE: config.stageName,\n    },\n    timeout: Duration.seconds(30),\n    memorySize: 512, // Give it enough memory for OpenSearch queries\n  })\n\n  // Grant Lambda permissions to read from S3\n  timingBucket.grantRead(timingMergerLambda)\n\n  // Grant Lambda permissions to read/write OpenSearch\n  config.opensearchDomain.grantReadWrite(timingMergerLambda)\n\n  // Add explicit policy for OpenSearch domain actions (needed for index operations)\n  timingMergerLambda.addToRolePolicy(\n    new PolicyStatement({\n      actions: [\n        'es:ESHttpGet',\n        'es:ESHttpPut',\n        'es:ESHttpPost',\n        'es:ESHttpHead',\n      ],\n      resources: [\n        config.opensearchDomain.domainArn,\n        `${config.opensearchDomain.domainArn}/*`,\n      ],\n    }),\n  )\n\n  // Configure S3 to send notifications to SQS on object creation\n  timingBucket.addEventNotification(\n    EventType.OBJECT_CREATED,\n    new SqsDestination(timingQueue),\n    {\n      // Filter for timing data objects (client/ or server/ prefix)\n      prefix: '',\n      suffix: '.json',\n    },\n  )\n\n  // Configure Lambda to consume from SQS\n  timingMergerLambda.addEventSource(\n    new SqsEventSource(timingQueue, {\n      batchSize: 10, // Process up to 10 messages at once\n      maxBatchingWindow: Duration.seconds(5), // Wait up to 5s to collect batch\n      reportBatchItemFailures: true, // Enable partial batch failure reporting\n    }),\n  )\n\n  return {\n    timingBucket,\n    timingMergerLambda,\n    timingQueue,\n    timingDLQ,\n  }\n}\n"
  },
  {
    "path": "server/infra/package.json",
    "content": "{\n  \"name\": \"ito-infra\",\n  \"version\": \"0.1.0\",\n  \"bin\": {\n    \"infra\": \"bin/infra.js\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc\",\n    \"watch\": \"tsc -w\",\n    \"test\": \"jest\",\n    \"cdk\": \"cdk\",\n    \"dev-cdk\": \"dotenv -e ../.env -- cdk\"\n  },\n  \"devDependencies\": {\n    \"@types/aws-lambda\": \"^8.10.145\",\n    \"@types/jest\": \"^29.5.14\",\n    \"@types/node\": \"22.7.9\",\n    \"aws-cdk-lib\": \"2.200.1\",\n    \"esbuild\": \"^0.25.12\",\n    \"jest\": \"^29.7.0\",\n    \"ts-jest\": \"^29.2.5\",\n    \"ts-node\": \"^10.9.2\",\n    \"typescript\": \"~5.6.3\"\n  },\n  \"dependencies\": {\n    \"@aws-sdk/client-ecs\": \"^3.844.0\",\n    \"@aws-sdk/client-s3\": \"^3.922.0\",\n    \"@aws-sdk/credential-provider-node\": \"^3.850.0\",\n    \"@opensearch-project/opensearch\": \"^2.13.0\",\n    \"aws-cdk-lib\": \"2.200.1\",\n    \"constructs\": \"^10.0.0\",\n    \"dotenv\": \"^17.2.1\",\n    \"dotenv-cli\": \"^10.0.0\",\n    \"global\": \"^4.4.0\",\n    \"pg\": \"^8.16.3\"\n  }\n}\n"
  },
  {
    "path": "server/infra/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"lib\": [\"es2022\"],\n    \"declaration\": true,\n    \"strict\": true,\n    \"noImplicitAny\": true,\n    \"strictNullChecks\": true,\n    \"noImplicitThis\": true,\n    \"alwaysStrict\": true,\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": false,\n    \"inlineSourceMap\": true,\n    \"inlineSources\": true,\n    \"experimentalDecorators\": true,\n    \"strictPropertyInitialization\": false,\n    \"typeRoots\": [\"./node_modules/@types\"]\n  },\n  \"exclude\": [\"node_modules\", \"cdk.out\"]\n}\n"
  },
  {
    "path": "server/package.json",
    "content": "{\n  \"name\": \"ito-server\",\n  \"version\": \"1.0.0\",\n  \"main\": \"dist/index.js\",\n  \"type\": \"module\",\n  \"license\": \"GPL-3.0-only\",\n  \"dependencies\": {\n    \"@auth0/auth0-fastify-api\": \"^1.0.2\",\n    \"@aws-sdk/client-cloudwatch-logs\": \"^3.723.0\",\n    \"@aws-sdk/client-s3\": \"^3.879.0\",\n    \"@bufbuild/buf\": \"^1.55.1\",\n    \"@bufbuild/protobuf\": \"^2.5.2\",\n    \"@bufbuild/protoc-gen-es\": \"^2.5.2\",\n    \"@bufbuild/protovalidate\": \"^0.6.0\",\n    \"@cerebras/cerebras_cloud_sdk\": \"^1.46.0\",\n    \"@connectrpc/connect\": \"^2.0.2\",\n    \"@connectrpc/connect-fastify\": \"^2.0.2\",\n    \"@connectrpc/connect-node\": \"^2.0.2\",\n    \"@connectrpc/protoc-gen-connect-es\": \"^1.6.1\",\n    \"@fastify/cors\": \"^11.1.0\",\n    \"config\": \"^4.0.0\",\n    \"dotenv\": \"^16.5.0\",\n    \"fastify\": \"^5.4.0\",\n    \"groq-sdk\": \"^0.26.0\",\n    \"node-pg-migrate\": \"^8.0.3\",\n    \"pg\": \"^8.16.3\",\n    \"stripe\": \"^19.2.0\",\n    \"uuid\": \"^11.1.0\",\n    \"zod\": \"^4.0.5\"\n  },\n  \"scripts\": {\n    \"build\": \"bunx tsc\",\n    \"clean\": \"rm -rf node_modules dist\",\n    \"start\": \"bun dist/index.js\",\n    \"proto:gen:server\": \"bunx buf generate --output=./src src/ito.proto && bunx buf generate --output=./src buf.build/bufbuild/protovalidate\",\n    \"proto:gen:client\": \"bunx buf generate --output=../app src/ito.proto && bunx buf generate --output=../app buf.build/bufbuild/protovalidate && cd .. && bun run format:fix:app\",\n    \"proto:gen\": \"bun run proto:gen:server && bun run proto:gen:client\",\n    \"db:migrate:create\": \"node-pg-migrate --migrations-dir src/migrations --migration-file-language js create\",\n    \"db:migrate\": \"bash ./scripts/migrate.sh up\",\n    \"db:migrate:down\": \"bash ./scripts/migrate.sh down\",\n    \"docker\": \"docker compose up --build\",\n    \"local-db-up\": \"docker compose up -d db\",\n    \"local-db-down\": \"docker compose stop db\",\n    \"local-s3-up\": \"docker compose up -d minio && sleep 3 && bash ./scripts/setup-minio.sh\",\n    \"local-s3-down\": \"docker compose stop minio\",\n    \"local-services-up\": \"bun local-db-up && bun local-s3-up\",\n    \"local-services-down\": \"bun local-db-down && bun local-s3-down\",\n    \"dev\": \"tsx watch src/index.ts\",\n    \"dev:win\": \"bun --watch src/index.ts\",\n    \"test-client\": \"tsx ./test-client.ts\",\n    \"migrate-audio-to-s3\": \"tsx scripts/migrate-audio-to-s3.ts\"\n  },\n  \"devDependencies\": {\n    \"@types/express\": \"^5.0.3\",\n    \"@types/node\": \"^24.0.1\",\n    \"@types/pg\": \"^8.15.4\",\n    \"@types/uuid\": \"^10.0.0\",\n    \"dotenv-cli\": \"^8.0.0\",\n    \"prettier\": \"^3.5.3\",\n    \"ts-node\": \"^10.9.2\",\n    \"tsx\": \"^4.20.2\",\n    \"typescript\": \"^5.8.3\"\n  }\n}\n"
  },
  {
    "path": "server/scripts/migrate-audio-to-s3.ts",
    "content": "#!/usr/bin/env tsx\n\n/**\n * Migration script to move raw audio blobs from PostgreSQL to S3\n * This script should be run after deploying the raw_audio_id column\n */\n\nimport pool from '../src/db.js'\nimport { getStorageClient } from '../src/clients/s3storageClient.js'\nimport { v4 as uuidv4 } from 'uuid'\nimport { createAudioKey } from '../src/constants/storage.js'\n\ninterface InteractionRow {\n  id: string\n  user_id: string | null\n  raw_audio: Buffer | null\n  raw_audio_id: string | null\n}\n\nasync function migrateAudioToS3() {\n  console.log('Starting audio migration to S3...')\n\n  try {\n    const storageClient = getStorageClient()\n\n    // Find all interactions with raw_audio but no raw_audio_id\n    const result = await pool.query<InteractionRow>(`\n      SELECT id, user_id, raw_audio, raw_audio_id \n      FROM interactions \n      WHERE raw_audio IS NOT NULL \n      AND raw_audio_id IS NULL \n      AND deleted_at IS NULL\n      ORDER BY created_at DESC\n    `)\n\n    const interactions = result.rows\n    console.log(`Found ${interactions.length} interactions to migrate`)\n\n    if (interactions.length === 0) {\n      console.log('No interactions need migration')\n      return\n    }\n\n    let migrated = 0\n    let failed = 0\n\n    for (const interaction of interactions) {\n      if (!interaction.raw_audio || !interaction.user_id) {\n        console.log(\n          `Skipping interaction ${interaction.id} - missing audio or user_id`,\n        )\n        continue\n      }\n\n      try {\n        // Generate UUID for this audio file\n        const audioUuid = uuidv4()\n        const audioKey = createAudioKey(interaction.user_id, audioUuid)\n\n        console.log(\n          `Migrating interaction ${interaction.id} (${interaction.raw_audio.length} bytes)`,\n        )\n\n        // Upload to S3\n        await storageClient.uploadObject(\n          audioKey,\n          interaction.raw_audio,\n          'audio/wav',\n          {\n            userId: interaction.user_id,\n            interactionId: interaction.id,\n            migratedAt: new Date().toISOString(),\n            originalSize: interaction.raw_audio.length.toString(),\n          },\n        )\n\n        // Update database with UUID and clear blob\n        await pool.query(\n          `UPDATE interactions \n           SET raw_audio_id = $1, raw_audio = NULL, updated_at = current_timestamp \n           WHERE id = $2`,\n          [audioUuid, interaction.id],\n        )\n\n        migrated++\n        console.log(`✅ Migrated interaction ${interaction.id}`)\n\n        // Add small delay to avoid overwhelming S3\n        await new Promise(resolve => setTimeout(resolve, 100))\n      } catch (error) {\n        console.error(\n          `❌ Failed to migrate interaction ${interaction.id}:`,\n          error,\n        )\n        failed++\n      }\n    }\n\n    console.log(`\\n🎉 Migration complete!`)\n    console.log(`✅ Migrated: ${migrated}`)\n    console.log(`❌ Failed: ${failed}`)\n    console.log(`📊 Total: ${interactions.length}`)\n\n    // Verify migration\n    const remainingResult = await pool.query(`\n      SELECT COUNT(*) as count \n      FROM interactions \n      WHERE raw_audio IS NOT NULL \n      AND raw_audio_id IS NULL \n      AND deleted_at IS NULL\n    `)\n\n    const remaining = parseInt(remainingResult.rows[0].count)\n    if (remaining > 0) {\n      console.log(`⚠️  Warning: ${remaining} interactions still need migration`)\n    } else {\n      console.log(`✅ All interactions have been migrated successfully`)\n    }\n  } catch (error) {\n    console.error('Migration failed:', error)\n    process.exit(1)\n  } finally {\n    await pool.end()\n  }\n}\n\n// Run if called directly\nif (import.meta.url === `file://${process.argv[1]}`) {\n  migrateAudioToS3().catch(console.error)\n}\n\nexport { migrateAudioToS3 }\n"
  },
  {
    "path": "server/scripts/migrate.sh",
    "content": "#!/bin/sh\n\n# Usage:\n#   ./scripts/migrate.sh up\n#   ./scripts/migrate.sh down\n\nset -e\n\nCOMMAND=$1\n\n# print usage if no command\nif [ -z \"$COMMAND\" ]; then\n  echo \"Usage: $0 <up|down>\"\n  exit 1\nfi\n\necho \"🌱 Running migrations: $COMMAND\"\n\nif [ -f .env ]; then\n  echo \"✅ Detected .env file, using dotenv to inject vars\"\n  bunx dotenv -e .env -- node ./node_modules/node-pg-migrate/bin/node-pg-migrate.js --migrations-dir src/migrations \"$COMMAND\"\nelse\n  echo \"✅ No .env file, sourcing DATABASE_URL from environment variables\"\n  export DATABASE_URL=postgres://$DB_USER:$DB_PASS@$DB_HOST:5432/$DB_NAME\n  node ./node_modules/node-pg-migrate/bin/node-pg-migrate.js --migrations-dir src/migrations \"$COMMAND\"\nfi"
  },
  {
    "path": "server/scripts/setup-minio.sh",
    "content": "#!/bin/bash\n\n# Wait for MinIO to be ready\necho \"Waiting for MinIO to start...\"\nsleep 5\n\n# Install mc (MinIO client) if not already installed\nif ! command -v mc &> /dev/null; then\n    echo \"Installing MinIO client...\"\n    \n    # Try macOS installation first\n    if [[ \"$OSTYPE\" == \"darwin\"* ]]; then\n        brew install minio/stable/mc 2>/dev/null || {\n            echo \"Failed to install via brew. Please install MinIO client manually:\"\n            echo \"  macOS: brew install minio/stable/mc\"\n            echo \"  Or download from: https://dl.min.io/client/mc/release/darwin-amd64/mc\"\n            exit 1\n        }\n    else\n        echo \"Please install MinIO client manually:\"\n        echo \"  macOS: brew install minio/stable/mc\"\n        echo \"  Linux: wget https://dl.min.io/client/mc/release/linux-amd64/mc && chmod +x mc && sudo mv mc /usr/local/bin/\"\n        echo \"  Windows (PowerShell): Invoke-WebRequest -Uri \\\"https://dl.min.io/client/mc/release/windows-amd64/mc.exe\\\" -OutFile \\\"mc.exe\\\"\"\n        echo \"           Then add mc.exe to your PATH or run from current directory\"\n        echo \"  Windows (Command Prompt): curl -o mc.exe https://dl.min.io/client/mc/release/windows-amd64/mc.exe\"\n        echo \"\"\n        echo \"Alternative: Use Docker to run mc commands:\"\n        echo \"  docker run --rm -it --entrypoint=/bin/sh minio/mc -c \\\"mc alias set local http://host.docker.internal:9000 minioadmin minioadmin && mc mb local/ito-audio-storage\\\"\"\n        exit 1\n    fi\nfi\n\n# Configure mc to connect to local MinIO\nmc alias set local http://localhost:9000 \"${S3_ACCESS_KEY_ID:-minioadmin}\" \"${S3_SECRET_ACCESS_KEY:-minioadmin}\"\n\n# Create blob storage bucket if it doesn't exist\nBLOB_BUCKET=${BLOB_STORAGE_BUCKET:-ito-blob-storage}\nif mc ls local/\"${BLOB_BUCKET}\" 2>/dev/null; then\n    echo \"Bucket ${BLOB_BUCKET} already exists\"\nelse\n    echo \"Creating bucket ${BLOB_BUCKET}...\"\n    mc mb local/\"${BLOB_BUCKET}\"\n    echo \"Bucket created successfully\"\nfi\n\n# Set bucket policy to allow read/write\nmc anonymous set download local/\"${BLOB_BUCKET}\" 2>/dev/null || true\n\n# Create timing storage bucket if it doesn't exist\nTIMING_BUCKET=${TIMING_BUCKET:-ito-timing-storage}\nif mc ls \"local/${TIMING_BUCKET}\" 2>/dev/null; then\n    echo \"Bucket ${TIMING_BUCKET} already exists\"\nelse\n    echo \"Creating bucket ${TIMING_BUCKET}...\"\n    mc mb \"local/${TIMING_BUCKET}\"\n    echo \"Bucket created successfully\"\nfi\n\n# Set bucket policy to allow read/write\nmc anonymous set download \"local/${TIMING_BUCKET}\" 2>/dev/null || true\n\necho \"MinIO setup complete!\"\necho \"MinIO Console: http://localhost:9001\"\necho \"S3 Endpoint: http://localhost:9000\"\necho \"Buckets:\"\necho \"  - ${BLOB_BUCKET} (blob storage)\"\necho \"  - ${TIMING_BUCKET} (timing data)\""
  },
  {
    "path": "server/src/auth/auth0Helpers.ts",
    "content": "export async function getAuth0ManagementToken(): Promise<string | null> {\n  const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN\n  const AUTH0_MGMT_CLIENT_ID = process.env.AUTH0_MGMT_CLIENT_ID\n  const AUTH0_MGMT_CLIENT_SECRET = process.env.AUTH0_MGMT_CLIENT_SECRET\n\n  if (!AUTH0_DOMAIN || !AUTH0_MGMT_CLIENT_ID || !AUTH0_MGMT_CLIENT_SECRET) {\n    return null\n  }\n\n  try {\n    const tokenUrl = `https://${AUTH0_DOMAIN}/oauth/token`\n    const res = await fetch(tokenUrl, {\n      method: 'POST',\n      headers: { 'content-type': 'application/json' },\n      body: JSON.stringify({\n        grant_type: 'client_credentials',\n        client_id: AUTH0_MGMT_CLIENT_ID,\n        client_secret: AUTH0_MGMT_CLIENT_SECRET,\n        audience: `https://${AUTH0_DOMAIN}/api/v2/`,\n      }),\n    })\n    const data: any = await res.json()\n    if (!res.ok || !data?.access_token) {\n      return null\n    }\n    return data.access_token as string\n  } catch {\n    return null\n  }\n}\n\nexport async function getUserInfoFromAuth0(\n  userSub: string,\n): Promise<{ email?: string; name?: string } | null> {\n  const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN\n  if (!AUTH0_DOMAIN) return null\n\n  const token = await getAuth0ManagementToken()\n  if (!token) return null\n\n  try {\n    const encodedSub = encodeURIComponent(userSub)\n    const url = `https://${AUTH0_DOMAIN}/api/v2/users/${encodedSub}`\n    const res = await fetch(url, {\n      headers: { Authorization: `Bearer ${token}` },\n    })\n\n    if (!res.ok) return null\n\n    const user = await res.json()\n    return {\n      email: user.email as string | undefined,\n      name: user.name as string | undefined,\n    }\n  } catch {\n    return null\n  }\n}\n"
  },
  {
    "path": "server/src/auth/userContext.ts",
    "content": "import { createContextKey } from '@connectrpc/connect'\n\n// Auth0 user type based on the user object structure\nexport interface Auth0User {\n  sub?: string\n  [key: string]: any\n}\n\n// Create a type-safe context key for the authenticated user\nexport const kUser = createContextKey<Auth0User | undefined>(undefined, {\n  description: 'Authenticated Auth0 user',\n})\n"
  },
  {
    "path": "server/src/clients/asrConfig.ts",
    "content": "export interface TranscriptionOptions {\n  fileType?: string\n  asrModel?: string\n  vocabulary?: string[]\n  noSpeechThreshold?: number\n}\n"
  },
  {
    "path": "server/src/clients/cerebrasClient.ts",
    "content": "import Cerebras from '@cerebras/cerebras_cloud_sdk'\nimport * as dotenv from 'dotenv'\nimport {\n  ClientApiKeyError,\n  ClientUnavailableError,\n  ClientApiError,\n} from './errors.js'\nimport { ClientProvider } from './providers.js'\nimport { LlmProvider } from './llmProvider.js'\nimport { TranscriptionOptions } from './asrConfig.js'\nimport { IntentTranscriptionOptions } from './intentTranscriptionConfig.js'\nimport { DEFAULT_ADVANCED_SETTINGS } from '../constants/generated-defaults.js'\n\n// Load environment variables from .env file\ndotenv.config()\nexport const itoVocabulary = ['Ito', 'Hey Ito']\n\n/**\n * A TypeScript client for interacting with the Cerebras API.\n */\nclass CerebrasClient implements LlmProvider {\n  private readonly _client: Cerebras\n  private readonly _userCommandModel: string\n  private readonly _isValid: boolean\n\n  constructor(apiKey: string, userCommandModel: string) {\n    if (!apiKey) {\n      throw new ClientApiKeyError(ClientProvider.CEREBRAS)\n    }\n    this._client = new Cerebras({ apiKey })\n    this._userCommandModel = userCommandModel\n    this._isValid = true\n  }\n\n  /**\n   * Checks if the client is configured correctly.\n   */\n  public get isAvailable(): boolean {\n    return this._isValid\n  }\n\n  /**\n   * Uses a thinking model to adjust/improve a transcript.\n   * @param transcript The original transcript text.\n   * @returns The adjusted transcript.\n   */\n  public async adjustTranscript(\n    userPrompt: string,\n    options?: IntentTranscriptionOptions,\n  ): Promise<string> {\n    if (!this.isAvailable) {\n      throw new ClientUnavailableError(ClientProvider.CEREBRAS)\n    }\n\n    const temperature = options?.temperature ?? 0.7\n    const model = options?.model || this._userCommandModel\n    const systemPrompt =\n      options?.prompt ||\n      'Adjust and improve this transcript for clarity and accuracy.'\n\n    try {\n      const completion = await this._client.chat.completions.create({\n        messages: [\n          {\n            role: 'system',\n            content: systemPrompt,\n          },\n          {\n            role: 'user',\n            content: userPrompt,\n          },\n        ],\n        model,\n        temperature,\n      })\n\n      return (completion.choices as any)[0]?.message?.content?.trim() || ' '\n    } catch (error: any) {\n      console.error('An error occurred during transcript adjustment:', error)\n      return userPrompt\n    }\n  }\n\n  /**\n   * Transcribes an audio buffer using the Cerebras API.\n   * Note: Cerebras currently focuses on text generation, not audio transcription.\n   * This implementation throws an error as audio transcription is not supported.\n   * @param audioBuffer The audio data as a Node.js Buffer.\n   * @param options Optional transcription configuration.\n   * @returns The transcribed text as a string.\n   */\n  public async transcribeAudio(\n    _audioBuffer: Buffer,\n    _options?: TranscriptionOptions,\n  ): Promise<string> {\n    if (!this.isAvailable) {\n      throw new ClientUnavailableError(ClientProvider.CEREBRAS)\n    }\n\n    // Cerebras doesn't currently support audio transcription\n    // We should throw an error indicating this feature is not supported\n    throw new ClientApiError(\n      'Audio transcription is not supported by Cerebras. Please use a different ASR provider.',\n      ClientProvider.CEREBRAS,\n      new Error('Feature not supported'),\n      501, // Not Implemented\n    )\n  }\n}\n\n// --- Singleton Instance ---\n// Create and export a single, pre-configured instance of the client for use across the server.\n// Check for CEREBRAS_API_KEY and create client if available\nconst apiKey = process.env.CEREBRAS_API_KEY\n\nlet cerebrasClient: CerebrasClient | null = null\n\nif (apiKey) {\n  try {\n    cerebrasClient = new CerebrasClient(\n      apiKey,\n      DEFAULT_ADVANCED_SETTINGS.llmModel,\n    )\n    console.log('Cerebras client initialized successfully')\n  } catch (error) {\n    console.error('Failed to initialize Cerebras client:', error)\n    cerebrasClient = null\n  }\n} else {\n  console.log(\n    'CEREBRAS_API_KEY not set - Cerebras client will not be available',\n  )\n  cerebrasClient = null\n}\n\nexport { cerebrasClient }\n"
  },
  {
    "path": "server/src/clients/errors.ts",
    "content": "import { create } from '@bufbuild/protobuf'\nimport {\n  ClientError as ClientErrorPb,\n  ClientErrorSchema,\n  ClientProvider as ClientProviderPb,\n  ErrorType as ErrorTypePb,\n} from '../generated/ito_pb.js'\nimport { ClientProvider } from './providers.js'\n\nexport enum ErrorType {\n  CONFIGURATION = 'configuration',\n  AVAILABILITY = 'availability',\n  AUDIO = 'audio',\n  API = 'api',\n}\n\n/**\n * Base error class for all client-related errors\n */\nexport abstract class ClientError extends Error {\n  abstract readonly code: string\n  abstract readonly type: ErrorType\n\n  constructor(\n    message: string,\n    public readonly provider: ClientProvider,\n    public readonly details?: Record<string, any>,\n  ) {\n    super(message)\n    this.name = this.constructor.name\n  }\n\n  /**\n   * Maps TypeScript ClientProvider enum to protobuf ClientProvider enum\n   */\n  private mapProviderToProtobuf(provider: ClientProvider): ClientProviderPb {\n    switch (provider) {\n      case ClientProvider.GROQ:\n        return ClientProviderPb.GROQ\n      case ClientProvider.CEREBRAS:\n        return ClientProviderPb.CEREBRAS\n      default:\n        return ClientProviderPb.GROQ\n    }\n  }\n\n  /**\n   * Maps TypeScript ErrorType enum to protobuf ErrorType enum\n   */\n  private mapErrorTypeToProtobuf(type: ErrorType): ErrorTypePb {\n    switch (type) {\n      case ErrorType.CONFIGURATION:\n        return ErrorTypePb.CONFIGURATION\n      case ErrorType.AVAILABILITY:\n        return ErrorTypePb.AVAILABILITY\n      case ErrorType.AUDIO:\n        return ErrorTypePb.AUDIO\n      case ErrorType.API:\n        return ErrorTypePb.API\n      default:\n        return ErrorTypePb.API\n    }\n  }\n\n  /**\n   * Converts this error to a protobuf ClientError\n   */\n  toProtobuf(): ClientErrorPb {\n    const details: Record<string, string> = {}\n\n    // Convert details to string map\n    if (this.details) {\n      for (const [key, value] of Object.entries(this.details)) {\n        details[key] = String(value)\n      }\n    }\n\n    return create(ClientErrorSchema, {\n      code: this.code,\n      type: this.mapErrorTypeToProtobuf(this.type),\n      message: this.message,\n      provider: this.mapProviderToProtobuf(this.provider),\n      details,\n    })\n  }\n}\n\n/**\n * Configuration-related errors\n */\nexport abstract class ClientConfigurationError extends ClientError {\n  readonly type = ErrorType.CONFIGURATION\n\n  constructor(\n    message: string,\n    provider: ClientProvider,\n    details?: Record<string, any>,\n  ) {\n    super(message, provider, details)\n  }\n}\n\n/**\n * API key missing or invalid\n */\nexport class ClientApiKeyError extends ClientConfigurationError {\n  readonly code = 'CLIENT_API_KEY_ERROR'\n\n  constructor(provider: ClientProvider) {\n    super('API key is required.', provider)\n  }\n}\n\n/**\n * Required model parameter missing\n */\nexport class ClientModelError extends ClientConfigurationError {\n  readonly code = 'CLIENT_MODEL_ERROR'\n\n  constructor(provider: ClientProvider) {\n    super('ASR model is required for transcription.', provider)\n  }\n}\n\n/**\n * Client availability errors\n */\nexport class ClientUnavailableError extends ClientError {\n  readonly code = 'CLIENT_UNAVAILABLE'\n  readonly type = ErrorType.AVAILABILITY\n\n  constructor(provider: ClientProvider) {\n    super('Client is not available. Check API key.', provider)\n  }\n}\n\n/**\n * Audio quality and transcription errors\n */\nexport abstract class ClientAudioError extends ClientError {\n  readonly type = ErrorType.AUDIO\n\n  constructor(\n    message: string,\n    provider: ClientProvider,\n    details?: Record<string, any>,\n  ) {\n    super(message, provider, details)\n  }\n}\n\n/**\n * No speech detected in audio\n */\nexport class ClientNoSpeechError extends ClientAudioError {\n  readonly code = 'CLIENT_NO_SPEECH_DETECTED'\n\n  constructor(\n    provider: ClientProvider,\n    public readonly noSpeechProbability?: number,\n  ) {\n    super('No speech detected in audio.', provider, { noSpeechProbability })\n  }\n}\n\n/**\n * Audio transcription quality too low\n */\nexport class ClientTranscriptionQualityError extends ClientAudioError {\n  readonly code = 'CLIENT_TRANSCRIPTION_QUALITY_ERROR'\n\n  constructor(\n    provider: ClientProvider,\n    public readonly averageLogProbability?: number,\n  ) {\n    super('Unable to transcribe audio.', provider, { averageLogProbability })\n  }\n}\n\n/**\n * Audio file too short for transcription\n */\nexport class ClientAudioTooShortError extends ClientAudioError {\n  readonly code = 'CLIENT_AUDIO_TOO_SHORT'\n\n  constructor(provider: ClientProvider) {\n    super('Audio file is too short for transcription.', provider, {})\n  }\n}\n\n/**\n * Client API service errors\n */\nexport class ClientApiError extends ClientError {\n  readonly code = 'CLIENT_API_ERROR'\n  readonly type = ErrorType.API\n\n  constructor(\n    message: string,\n    provider: ClientProvider,\n    public readonly originalError?: Error,\n    public readonly statusCode?: number,\n  ) {\n    super(message, provider, {\n      originalError: originalError?.message,\n      statusCode,\n    })\n  }\n}\n\n/**\n * Type guard to check if an error is a client-related error\n */\nexport function isClientError(error: unknown): error is ClientError {\n  return error instanceof ClientError\n}\n\n/**\n * Type guard to check if an error is a specific type of client error\n */\nexport function isClientErrorType<T extends ClientError>(\n  error: unknown,\n  ErrorClass: new (...args: any[]) => T,\n): error is T {\n  return error instanceof ErrorClass\n}\n\n/**\n * Converts any error to a protobuf ClientError\n * If it's already a ClientError, converts directly\n * Otherwise, creates a generic API error\n */\nexport function errorToProtobuf(\n  error: unknown,\n  provider: ClientProvider = ClientProvider.GROQ,\n): ClientErrorPb {\n  if (isClientError(error)) {\n    return error.toProtobuf()\n  }\n\n  // Create a generic API error for unknown errors\n  const genericError = new ClientApiError(\n    error instanceof Error ? error.message : 'An unknown error occurred',\n    provider,\n    error instanceof Error ? error : undefined,\n  )\n\n  return genericError.toProtobuf()\n}\n"
  },
  {
    "path": "server/src/clients/groqClient.test.ts",
    "content": "import { describe, it, expect, mock, beforeEach, afterAll } from 'bun:test'\n\n// Mock environment variables before any imports\nconst originalEnv = process.env\nprocess.env = {\n  ...originalEnv,\n  GROQ_API_KEY: 'test-api-key',\n}\n\n// Mock the Groq SDK\nconst mockGroqClient = {\n  audio: {\n    transcriptions: {\n      create: mock(),\n    },\n  },\n  chat: {\n    completions: {\n      create: mock(),\n    },\n  },\n}\n\n// Mock the groq-sdk module\nmock.module('groq-sdk', () => ({\n  default: class MockGroq {\n    constructor() {\n      return mockGroqClient\n    }\n  },\n}))\n\n// Mock the toFile function\nmock.module('groq-sdk/uploads', () => ({\n  toFile: mock((buffer: Buffer, filename: string) =>\n    Promise.resolve({ buffer, filename }),\n  ),\n}))\n\n// Mock dotenv to prevent .env file loading\nmock.module('dotenv', () => ({\n  config: mock(() => ({})),\n}))\n\n// Now we can safely import the groqClient\nconst { groqClient, itoVocabulary } = await import('./groqClient.js')\nconst { createTranscriptionPrompt } = await import(\n  '../prompts/transcription.js'\n)\n\ndescribe('GroqClient', () => {\n  const NO_SPEECH_THRESHOLD = 0.6\n  beforeEach(() => {\n    // Clear all mocks before each test\n    mockGroqClient.audio.transcriptions.create.mockClear()\n    mockGroqClient.chat.completions.create.mockClear()\n  })\n\n  afterAll(() => {\n    // Restore original environment\n    process.env = originalEnv\n  })\n\n  describe('transcribeAudio', () => {\n    it('should use the provided ASR model for transcription', async () => {\n      const mockTranscription = { text: 'Hello world' }\n      mockGroqClient.audio.transcriptions.create.mockResolvedValue(\n        mockTranscription,\n      )\n\n      const audioBuffer = Buffer.from('mock audio data')\n      const asrModel = 'whisper-large-v3'\n      const vocabulary = ['hello', 'world']\n      const transcriptionPrompt = createTranscriptionPrompt([\n        ...itoVocabulary,\n        ...vocabulary,\n      ])\n\n      const result = await groqClient.transcribeAudio(audioBuffer, {\n        fileType: 'wav',\n        asrModel,\n        noSpeechThreshold: NO_SPEECH_THRESHOLD,\n        vocabulary,\n      })\n\n      expect(result).toBe('Hello world')\n      expect(mockGroqClient.audio.transcriptions.create).toHaveBeenCalledWith({\n        file: expect.objectContaining({\n          filename: 'audio.wav',\n        }),\n        model: asrModel,\n        prompt: transcriptionPrompt,\n        response_format: 'verbose_json',\n      })\n    })\n\n    it('should use default file type when not specified', async () => {\n      const mockTranscription = { text: 'Test transcription' }\n      mockGroqClient.audio.transcriptions.create.mockResolvedValue(\n        mockTranscription,\n      )\n\n      const audioBuffer = Buffer.from('mock audio data')\n      const asrModel = 'distil-whisper-large-v3-en'\n      const transcriptionPrompt = createTranscriptionPrompt(itoVocabulary)\n\n      await groqClient.transcribeAudio(audioBuffer, {\n        asrModel,\n        noSpeechThreshold: NO_SPEECH_THRESHOLD,\n      })\n\n      expect(mockGroqClient.audio.transcriptions.create).toHaveBeenCalledWith({\n        file: expect.objectContaining({\n          filename: 'audio.webm',\n        }),\n        model: asrModel,\n        prompt: transcriptionPrompt,\n        response_format: 'verbose_json',\n      })\n    })\n\n    it('should handle vocabulary properly', async () => {\n      const mockTranscription = { text: 'Custom vocabulary test' }\n      mockGroqClient.audio.transcriptions.create.mockResolvedValue(\n        mockTranscription,\n      )\n\n      const audioBuffer = Buffer.from('mock audio data')\n      const asrModel = 'whisper-large-v3'\n      const vocabulary = ['custom', 'vocabulary', 'test']\n      const transcriptionPrompt = createTranscriptionPrompt([\n        ...itoVocabulary,\n        ...vocabulary,\n      ])\n\n      await groqClient.transcribeAudio(audioBuffer, {\n        fileType: 'wav',\n        asrModel,\n        noSpeechThreshold: NO_SPEECH_THRESHOLD,\n        vocabulary,\n      })\n\n      expect(mockGroqClient.audio.transcriptions.create).toHaveBeenCalledWith({\n        file: expect.objectContaining({\n          filename: 'audio.wav',\n        }),\n        model: asrModel,\n        prompt: transcriptionPrompt,\n        response_format: 'verbose_json',\n      })\n    })\n\n    it('should handle empty vocabulary', async () => {\n      const mockTranscription = { text: 'No vocabulary' }\n      mockGroqClient.audio.transcriptions.create.mockResolvedValue(\n        mockTranscription,\n      )\n\n      const audioBuffer = Buffer.from('mock audio data')\n      const asrModel = 'whisper-large-v3'\n      const transcriptionPrompt = createTranscriptionPrompt(itoVocabulary)\n\n      await groqClient.transcribeAudio(audioBuffer, {\n        fileType: 'wav',\n        asrModel,\n        noSpeechThreshold: NO_SPEECH_THRESHOLD,\n        vocabulary: [],\n      })\n\n      expect(mockGroqClient.audio.transcriptions.create).toHaveBeenCalledWith({\n        file: expect.objectContaining({\n          filename: 'audio.wav',\n        }),\n        model: asrModel,\n        prompt: transcriptionPrompt,\n        response_format: 'verbose_json',\n      })\n    })\n\n    it('should throw error when ASR model is not provided', async () => {\n      const audioBuffer = Buffer.from('mock audio data')\n\n      await expect(\n        groqClient.transcribeAudio(audioBuffer, {\n          fileType: 'wav',\n          asrModel: '',\n          noSpeechThreshold: NO_SPEECH_THRESHOLD,\n        }),\n      ).rejects.toThrow('ASR model is required for transcription.')\n    })\n\n    it('should trim whitespace from transcription result', async () => {\n      const mockTranscription = { text: '  Hello world  ' }\n      mockGroqClient.audio.transcriptions.create.mockResolvedValue(\n        mockTranscription,\n      )\n\n      const audioBuffer = Buffer.from('mock audio data')\n      const asrModel = 'whisper-large-v3'\n\n      const result = await groqClient.transcribeAudio(audioBuffer, {\n        fileType: 'wav',\n        asrModel,\n        noSpeechThreshold: NO_SPEECH_THRESHOLD,\n      })\n\n      expect(result).toBe('Hello world')\n    })\n\n    it('should throw when the transcript contains no speech', async () => {\n      const mockTranscription = {\n        text: '',\n        segments: [{ no_speech_prob: NO_SPEECH_THRESHOLD + 0.01 }],\n      }\n      mockGroqClient.audio.transcriptions.create.mockResolvedValue(\n        mockTranscription,\n      )\n      const audioBuffer = Buffer.from('mock audio data')\n      const asrModel = 'whisper-large-v3'\n\n      await expect(\n        groqClient.transcribeAudio(audioBuffer, {\n          fileType: 'wav',\n          asrModel,\n          noSpeechThreshold: NO_SPEECH_THRESHOLD,\n        }),\n      ).rejects.toThrow('No speech detected')\n    })\n\n    it('should handle Groq API errors properly', async () => {\n      const mockError = new Error('Groq API error')\n      mockGroqClient.audio.transcriptions.create.mockRejectedValue(mockError)\n\n      const audioBuffer = Buffer.from('mock audio data')\n      const asrModel = 'whisper-large-v3'\n\n      await expect(\n        groqClient.transcribeAudio(audioBuffer, {\n          fileType: 'wav',\n          asrModel,\n          noSpeechThreshold: NO_SPEECH_THRESHOLD,\n        }),\n      ).rejects.toThrow('Groq API error')\n    })\n  })\n\n  describe('adjustTranscript', () => {\n    beforeEach(() => {\n      mockGroqClient.chat.completions.create.mockReset()\n    })\n\n    it('should use LLM to adjust transcript', async () => {\n      const mockCompletion = {\n        choices: [\n          {\n            message: {\n              content: 'Adjusted transcript content',\n            },\n          },\n        ],\n      }\n      mockGroqClient.chat.completions.create.mockResolvedValue(mockCompletion)\n\n      const originalTranscript = 'Original transcript'\n      const result = await groqClient.adjustTranscript(originalTranscript, {\n        temperature: 0.1,\n        model: 'llama-3.3-70b-versatile',\n        prompt:\n          'You are a dictation assistant named Ito. Your job is to fulfill the intent of the transcript without asking follow up questions.',\n      })\n\n      expect(result).toBe('Adjusted transcript content')\n      expect(mockGroqClient.chat.completions.create).toHaveBeenCalledWith({\n        messages: [\n          {\n            role: 'system',\n            content:\n              'You are a dictation assistant named Ito. Your job is to fulfill the intent of the transcript without asking follow up questions.',\n          },\n          {\n            role: 'user',\n            content: originalTranscript,\n          },\n        ],\n        model: 'llama-3.3-70b-versatile',\n        temperature: 0.1,\n      })\n    })\n\n    it('should return user prompt on LLM error', async () => {\n      const mockError = new Error('LLM API error')\n      mockGroqClient.chat.completions.create.mockRejectedValue(mockError)\n\n      const originalTranscript = 'Original transcript'\n      const result = await groqClient.adjustTranscript(originalTranscript, {\n        temperature: 0.1,\n        model: 'llama-3.3-70b-versatile',\n        prompt:\n          'You are a dictation assistant named Ito. Your job is to fulfill the intent of the transcript without asking follow up questions.',\n      })\n\n      expect(result).toBe(originalTranscript)\n    })\n\n    it('should handle empty LLM response', async () => {\n      const mockCompletion = {\n        choices: [\n          {\n            message: {\n              content: null,\n            },\n          },\n        ],\n      }\n      mockGroqClient.chat.completions.create.mockResolvedValue(mockCompletion)\n\n      const originalTranscript = 'Original transcript'\n      const result = await groqClient.adjustTranscript(originalTranscript, {\n        temperature: 0.1,\n        model: 'llama-3.3-70b-versatile',\n        prompt:\n          'You are a dictation assistant named Ito. Your job is to fulfill the intent of the transcript without asking follow up questions.',\n      })\n\n      expect(result).toBe(' ')\n    })\n  })\n})\n"
  },
  {
    "path": "server/src/clients/groqClient.ts",
    "content": "import Groq from 'groq-sdk'\nimport { toFile } from 'groq-sdk/uploads'\nimport * as dotenv from 'dotenv'\nimport { createTranscriptionPrompt } from '../prompts/transcription.js'\nimport {\n  ClientApiKeyError,\n  ClientUnavailableError,\n  ClientModelError,\n  ClientNoSpeechError,\n  ClientAudioTooShortError,\n  ClientApiError,\n  ClientError,\n} from './errors.js'\nimport { ClientProvider } from './providers.js'\nimport { LlmProvider } from './llmProvider.js'\nimport { TranscriptionOptions } from './asrConfig.js'\nimport { IntentTranscriptionOptions } from './intentTranscriptionConfig.js'\nimport { DEFAULT_ADVANCED_SETTINGS } from '../constants/generated-defaults.js'\n\n// Load environment variables from .env file\ndotenv.config()\nexport const itoVocabulary = ['Ito', 'Hey Ito']\n\n/**\n * A TypeScript client for interacting with the Groq API, inspired by your Python implementation.\n */\nclass GroqClient implements LlmProvider {\n  private readonly _client: Groq\n  private readonly _userCommandModel: string\n  private readonly _isValid: boolean\n\n  constructor(apiKey: string, userCommandModel: string) {\n    if (!apiKey) {\n      throw new ClientApiKeyError(ClientProvider.GROQ)\n    }\n    this._client = new Groq({ apiKey })\n    this._userCommandModel = userCommandModel\n    this._isValid = true\n  }\n\n  /**\n   * Checks if the client is configured correctly.\n   */\n  public get isAvailable(): boolean {\n    return this._isValid\n  }\n\n  /**\n   * Uses a thinking model to adjust/improve a transcript.\n   * @param transcript The original transcript text.\n   * @returns The adjusted transcript.\n   */\n  public async adjustTranscript(\n    userPrompt: string,\n    options?: IntentTranscriptionOptions,\n  ): Promise<string> {\n    if (!this.isAvailable) {\n      throw new ClientUnavailableError(ClientProvider.GROQ)\n    }\n\n    const temperature = options?.temperature ?? 0.7\n    const model = options?.model || this._userCommandModel\n    const systemPrompt =\n      options?.prompt ||\n      'Adjust and improve this transcript for clarity and accuracy.'\n\n    try {\n      const completion = await this._client.chat.completions.create({\n        messages: [\n          {\n            role: 'system',\n            content: systemPrompt,\n          },\n          {\n            role: 'user',\n            content: userPrompt,\n          },\n        ],\n        model,\n        temperature,\n      })\n\n      // Return a space to enable emptying the document\n      return completion.choices[0]?.message?.content?.trim() || ' '\n    } catch (error: any) {\n      console.error('An error occurred during transcript adjustment:', error)\n      return userPrompt\n    }\n  }\n\n  /**\n   * Transcribes an audio buffer using the Groq API.\n   * @param audioBuffer The audio data as a Node.js Buffer.\n   * @param options Optional transcription configuration.\n   * @returns The transcribed text as a string.\n   */\n  public async transcribeAudio(\n    audioBuffer: Buffer,\n    options?: TranscriptionOptions,\n  ): Promise<string> {\n    console.log('Transcribing audio with options:', options)\n    const fileType = options?.fileType || 'webm'\n    const asrModel = options?.asrModel\n    const vocabulary = options?.vocabulary\n    const noSpeechThreshold =\n      options?.noSpeechThreshold ?? DEFAULT_ADVANCED_SETTINGS.noSpeechThreshold\n\n    const file = await toFile(audioBuffer, `audio.${fileType}`)\n    if (!this.isAvailable) {\n      throw new ClientUnavailableError(ClientProvider.GROQ)\n    }\n    if (!asrModel) {\n      throw new ClientModelError(ClientProvider.GROQ)\n    }\n\n    try {\n      console.log(\n        `Transcribing ${audioBuffer.length} bytes of audio using model ${asrModel}...`,\n      )\n\n      const fullVocabulary = [...itoVocabulary, ...(vocabulary || [])]\n\n      // Create a concise but effective transcription prompt\n      const transcriptionPrompt = createTranscriptionPrompt(fullVocabulary)\n\n      const transcription = await this._client.audio.transcriptions.create({\n        // The toFile helper correctly handles buffers for multipart/form-data uploads.\n        // Providing a filename with the correct extension is crucial for the API.\n        file,\n        model: asrModel,\n        prompt: transcriptionPrompt,\n        response_format: 'verbose_json',\n      })\n\n      const segments = (transcription as any).segments\n      if (segments && segments.length > 0) {\n        const first = segments[0]\n        if (first?.no_speech_prob > noSpeechThreshold) {\n          console.log('No speech probability:', first.no_speech_prob)\n          throw new ClientNoSpeechError(\n            ClientProvider.GROQ,\n            first.no_speech_prob,\n          )\n        }\n      }\n\n      // The Node SDK returns the full object, the text is in the `text` property.\n      return transcription.text.trim()\n    } catch (error: any) {\n      console.log(\n        `Failed to transcribe audio of size ${audioBuffer.length} bytes.`,\n      )\n      console.error('An error occurred during Groq transcription:', error)\n      if (error instanceof ClientError) {\n        throw error\n      }\n\n      const errorMessage = error.message || 'An unknown error occurred'\n\n      // Check for specific audio too short error\n      if (errorMessage.includes('Audio file is too short')) {\n        throw new ClientAudioTooShortError(ClientProvider.GROQ)\n      }\n\n      // Re-throw the error to be handled by the caller (e.g., the gRPC service handler).\n      throw new ClientApiError(\n        errorMessage,\n        ClientProvider.GROQ,\n        error,\n        error.status || error.statusCode,\n      )\n    }\n  }\n}\n\n// --- Singleton Instance ---\n// Create and export a single, pre-configured instance of the client for use across the server.\n// Only check for GROQ_API_KEY since ASR model is now provided per-request\nif (!process.env.GROQ_API_KEY) {\n  console.error(\n    'FATAL: GROQ_API_KEY is not set in the .env file. The application cannot start.',\n  )\n  process.exit(1)\n}\nconst apiKey = process.env.GROQ_API_KEY\n\n// Note: userCommandModel is empty for now as we are only using transcription.\nexport const groqClient = new GroqClient(apiKey, '')\n"
  },
  {
    "path": "server/src/clients/intentTranscriptionConfig.ts",
    "content": "export interface IntentTranscriptionOptions {\n  temperature?: number\n  model?: string\n  prompt?: string\n}\n"
  },
  {
    "path": "server/src/clients/llmProvider.ts",
    "content": "import { TranscriptionOptions } from './asrConfig.js'\nimport { IntentTranscriptionOptions } from './intentTranscriptionConfig.js'\n\nexport interface LlmProvider {\n  readonly isAvailable: boolean\n\n  transcribeAudio(\n    audioBuffer: Buffer,\n    options?: TranscriptionOptions,\n  ): Promise<string>\n\n  adjustTranscript(\n    userPrompt: string,\n    options?: IntentTranscriptionOptions,\n  ): Promise<string>\n}\n"
  },
  {
    "path": "server/src/clients/providerUtils.ts",
    "content": "import { LlmProvider } from './llmProvider.js'\nimport { ClientProvider } from './providers.js'\nimport { groqClient } from './groqClient.js'\nimport { cerebrasClient } from './cerebrasClient.js'\nimport { ClientUnavailableError } from './errors.js'\n\n/**\n * Get an ASR provider by name\n * @param providerName The name of the ASR provider\n * @returns The ASR provider instance\n */\nexport function getAsrProvider(providerName: string): LlmProvider {\n  switch (providerName) {\n    case ClientProvider.GROQ:\n      if (!groqClient.isAvailable) {\n        throw new ClientUnavailableError(ClientProvider.GROQ)\n      }\n      return groqClient\n\n    default:\n      throw new ClientUnavailableError(providerName as ClientProvider)\n  }\n}\n\n/**\n * Get an LLM provider by name\n * @param providerName The name of the LLM provider\n * @returns The LLM provider instance\n */\nexport function getLlmProvider(providerName: string): LlmProvider {\n  switch (providerName) {\n    case ClientProvider.GROQ:\n      if (!groqClient.isAvailable) {\n        throw new ClientUnavailableError(ClientProvider.GROQ)\n      }\n      return groqClient\n\n    case ClientProvider.CEREBRAS:\n      if (!cerebrasClient || !cerebrasClient.isAvailable) {\n        throw new ClientUnavailableError(ClientProvider.CEREBRAS)\n      }\n      return cerebrasClient\n\n    default:\n      throw new ClientUnavailableError(providerName as ClientProvider)\n  }\n}\n\n/**\n * Get list of available ASR providers\n * @returns Array of available ASR provider names\n */\nexport function getAvailableAsrProviders(): ClientProvider[] {\n  const providers: ClientProvider[] = []\n\n  if (groqClient.isAvailable) {\n    providers.push(ClientProvider.GROQ)\n  }\n\n  return providers\n}\n\n/**\n * Get list of available LLM providers\n * @returns Array of available LLM provider names\n */\nexport function getAvailableLlmProviders(): ClientProvider[] {\n  const providers: ClientProvider[] = []\n\n  if (groqClient.isAvailable) {\n    providers.push(ClientProvider.GROQ)\n  }\n\n  if (cerebrasClient && cerebrasClient.isAvailable) {\n    providers.push(ClientProvider.CEREBRAS)\n  }\n\n  return providers\n}\n"
  },
  {
    "path": "server/src/clients/providers.ts",
    "content": "export enum ClientProvider {\n  GROQ = 'groq',\n  CEREBRAS = 'cerebras',\n}\n"
  },
  {
    "path": "server/src/clients/s3storageClient.ts",
    "content": "import {\n  S3Client,\n  PutObjectCommand,\n  GetObjectCommand,\n  DeleteObjectCommand,\n  ListObjectsV2Command,\n  HeadObjectCommand,\n  PutObjectCommandInput,\n  GetObjectCommandInput,\n  DeleteObjectCommandInput,\n  ListObjectsV2CommandInput,\n  HeadObjectCommandInput,\n  DeleteObjectsCommand,\n} from '@aws-sdk/client-s3'\nimport { Readable } from 'stream'\n\nexport class S3StorageClient {\n  private s3Client: S3Client\n  private bucketName: string\n  private bucketChecked: boolean = false\n\n  constructor(bucketName?: string) {\n    const bucket = bucketName\n    if (!bucket) {\n      throw new Error(\n        'Bucket name not provided and BLOB_STORAGE_BUCKET environment variable is not set',\n      )\n    }\n\n    this.bucketName = bucket\n\n    // Configure S3 client with support for MinIO/local development\n    const s3Config: any = {\n      region: process.env.AWS_REGION || 'us-west-2',\n    }\n\n    // If S3_ENDPOINT is set, we're using MinIO or another S3-compatible service\n    if (process.env.S3_ENDPOINT) {\n      s3Config.endpoint = process.env.S3_ENDPOINT\n      s3Config.forcePathStyle = process.env.S3_FORCE_PATH_STYLE === 'true'\n\n      // Use explicit credentials for local development\n      if (process.env.S3_ACCESS_KEY_ID && process.env.S3_SECRET_ACCESS_KEY) {\n        s3Config.credentials = {\n          accessKeyId: process.env.S3_ACCESS_KEY_ID,\n          secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,\n        }\n      }\n    }\n\n    this.s3Client = new S3Client(s3Config)\n  }\n\n  async uploadObject(\n    key: string,\n    body: Buffer | Uint8Array | string | Readable,\n    contentType?: string,\n    metadata?: Record<string, string>,\n  ): Promise<void> {\n    const params: PutObjectCommandInput = {\n      Bucket: this.bucketName,\n      Key: key,\n      Body: body,\n      ContentType: contentType,\n      Metadata: metadata,\n    }\n\n    await this.s3Client.send(new PutObjectCommand(params))\n  }\n\n  async getObject(key: string): Promise<{\n    body: Readable | undefined\n    contentType?: string\n    metadata?: Record<string, string>\n  }> {\n    const params: GetObjectCommandInput = {\n      Bucket: this.bucketName,\n      Key: key,\n    }\n\n    const response = await this.s3Client.send(new GetObjectCommand(params))\n\n    return {\n      body: response.Body as Readable,\n      contentType: response.ContentType,\n      metadata: response.Metadata,\n    }\n  }\n\n  async deleteObject(key: string): Promise<void> {\n    const params: DeleteObjectCommandInput = {\n      Bucket: this.bucketName,\n      Key: key,\n    }\n\n    await this.s3Client.send(new DeleteObjectCommand(params))\n  }\n\n  async listObjects(\n    prefix?: string,\n    maxKeys?: number,\n  ): Promise<{\n    keys: string[]\n    isTruncated: boolean\n  }> {\n    const params: ListObjectsV2CommandInput = {\n      Bucket: this.bucketName,\n      Prefix: prefix,\n      MaxKeys: maxKeys,\n    }\n\n    const response = await this.s3Client.send(new ListObjectsV2Command(params))\n\n    return {\n      keys: response.Contents?.map(item => item.Key!).filter(Boolean) || [],\n      isTruncated: response.IsTruncated || false,\n    }\n  }\n\n  async hardDeletePrefix(prefix: string): Promise<number> {\n    let deletedCount = 0\n    let continuationToken: string | undefined\n\n    do {\n      const listParams: ListObjectsV2CommandInput = {\n        Bucket: this.bucketName,\n        Prefix: prefix,\n        ContinuationToken: continuationToken,\n        MaxKeys: 1000,\n      }\n      const response = await this.s3Client.send(\n        new ListObjectsV2Command(listParams),\n      )\n\n      const keys = (response.Contents ?? [])\n        .map(obj => obj.Key!)\n        .filter(Boolean)\n\n      await this.s3Client.send(\n        new DeleteObjectsCommand({\n          Bucket: this.bucketName,\n          Delete: { Objects: keys.map(k => ({ Key: k })) },\n        }),\n      )\n\n      deletedCount += keys.length\n\n      continuationToken = response.IsTruncated\n        ? response.NextContinuationToken\n        : undefined\n    } while (continuationToken)\n\n    return deletedCount\n  }\n\n  async objectExists(key: string): Promise<boolean> {\n    const params: HeadObjectCommandInput = {\n      Bucket: this.bucketName,\n      Key: key,\n    }\n\n    try {\n      await this.s3Client.send(new HeadObjectCommand(params))\n      return true\n    } catch (error: any) {\n      if (\n        error.name === 'NotFound' ||\n        error.$metadata?.httpStatusCode === 404\n      ) {\n        return false\n      }\n      throw error\n    }\n  }\n\n  async getObjectUrl(key: string, _expiresIn?: number): Promise<string> {\n    // For public buckets or when using CloudFront\n    // TODO: Implement presigned URL generation when needed\n    return `https://${this.bucketName}.s3.amazonaws.com/${key}`\n  }\n\n  getBucketName(): string {\n    return this.bucketName\n  }\n}\n\n// Singleton instance\nlet storageClient: S3StorageClient | null = null\n\nexport function getStorageClient(): S3StorageClient {\n  const bucketName = process.env.BLOB_STORAGE_BUCKET\n  if (!storageClient) {\n    storageClient = new S3StorageClient(bucketName)\n  }\n  return storageClient\n}\n"
  },
  {
    "path": "server/src/constants/generated-defaults.ts",
    "content": "/*\n * AUTO-GENERATED FILE - DO NOT EDIT\n * Generated from /shared-constants.js\n * Run 'bun generate:constants' to regenerate\n */\n\nexport const DEFAULT_ADVANCED_SETTINGS = {\n  // ASR (Automatic Speech Recognition) settings\n  asrProvider: 'groq',\n  asrModel: 'whisper-large-v3',\n  asrPrompt: ``,\n\n  // LLM (Large Language Model) settings\n  llmProvider: 'groq',\n  llmModel: 'openai/gpt-oss-120b',\n  llmTemperature: 0.1,\n\n  // Prompt settings\n  transcriptionPrompt: `You are a real-time Transcript Polisher assistant. Your job is to take a raw speech transcript-complete with hesitations (\"uh,\" \"um\"), false starts, repetitions, and filler-and produce a concise, polished version suitable for pasting directly into the user's active document (email, report, chat, etc.).\n\n- Keep the user's meaning and tone intact: don't introduce ideas or change intent.\n- Remove disfluencies: delete \"uh,\" \"um,\" \"you know,\" repeated words, and false starts.\n- Resolve corrections smoothly: when the speaker self-corrects (\"let's do next week... no, next month\"), choose the final phrasing.\n- Preserve natural phrasing: maintain contractions and informal tone if present, unless clarity demands adjustment.\n- Maintain accuracy: do not invent or omit key details like dates, names, or numbers.\n- Produce clean prose: use complete sentences, correct punctuation, and paragraph breaks only where needed for readability.\n- Operate within a single reply: output only the cleaned text-no commentary, meta-notes, or apologies.\n\nExample\nRaw transcript:\n\"Uhhh, so, I was thinking... maybe we could-uh-shoot for Thursday morning? No, actually, let's aim for the first week of May.\"\n\nCleaned output:\n\"Let's schedule the meeting for the first week of May.\"\n\nWhen you receive a transcript, immediately return the polished version following these rules.\n`,\n  editingPrompt: ` You are a Command-Interpreter assistant. Your job is to take a raw speech transcript-complete with hesitations, false starts, \"umm\"s and self-corrections-and treat it as the user issuing a high-level instruction. Instead of merely polishing their words, you must:\n    1.\tExtract the intent: identify the action the user is asking for (e.g. \"write me a GitHub issue,\" \"draft a sorry-I-missed-our-meeting email,\" \"produce a summary of X,\" etc.).\n    2.\tIgnore disfluencies: strip out \"uh,\" \"um,\" false starts and filler so you see only the core command.\n    3.\tMap to a template: choose an appropriate standard format (GitHub issue markdown template, professional email, bullet-point agenda, etc.) that matches the intent.\n    4.\tGenerate the deliverable: produce a fully-formed document in that format, filling in placeholders sensibly from any details in the transcript.\n    5.\tDo not add new intent: if the transcript doesn't specify something (e.g. title, recipients, date), use reasonable defaults (e.g. \"Untitled Issue,\" \"To: [Recipient]\") or prompt the user for the missing piece.\n    6.\tProduce only the final document: no commentary, apologies, or side-notes-just the completed issue/email/summary/etc.\n    7. Your response MUST contain ONLY the resultant text. DO NOT include:\n      - Any markers like [START/END CURRENT NOTES CONTENT]\n      - Any explanations, apologies, or additional text\n      - Any formatting markers like --- or \\`\\`\\`\n  `,\n\n  // Audio quality thresholds\n  noSpeechThreshold: 0.6,\n} as const\n"
  },
  {
    "path": "server/src/constants/markers.ts",
    "content": "export const START_WINDOW_TITLE_MARKER = '{START_WINDOW_TITLE_MARKER}'\nexport const END_WINDOW_TITLE_MARKER = '{END_WINDOW_TITLE_MARKER}'\nexport const START_APP_NAME_MARKER = '{START_APP_NAME_MARKER}'\nexport const END_APP_NAME_MARKER = '{END_APP_NAME_MARKER}'\nexport const START_USER_COMMAND_MARKER = '{START_USER_COMMAND_MARKER}'\nexport const END_USER_COMMAND_MARKER = '{END_USER_COMMAND_MARKER}'\nexport const START_CONTEXT_MARKER = '{START_CONTEXT_MARKER}'\nexport const END_CONTEXT_MARKER = '{END_CONTEXT_MARKER}'\n"
  },
  {
    "path": "server/src/constants/storage.ts",
    "content": "export const AUDIO_KEY_PREFIX = 'raw-audio'\n\nexport function createAudioKey(userId: string, audioUuid: string): string {\n  return `${AUDIO_KEY_PREFIX}/${userId}/${audioUuid}`\n}\n"
  },
  {
    "path": "server/src/db/models.ts",
    "content": "export interface Note {\n  id: string\n  user_id: string\n  interaction_id: string | null\n  content: string\n  created_at: Date\n  updated_at: Date\n  deleted_at: Date | null\n}\n\nexport interface Interaction {\n  id: string\n  user_id: string | null\n  title: string | null\n  asr_output: any\n  llm_output: any\n  raw_audio: Buffer | null\n  raw_audio_id: string | null\n  duration_ms: number | null\n  created_at: Date\n  updated_at: Date\n  deleted_at: Date | null\n}\n\nexport interface DictionaryItem {\n  id: string\n  user_id: string\n  word: string\n  pronunciation: string | null\n  created_at: Date\n  updated_at: Date\n  deleted_at: Date | null\n}\n\ninterface LlmSettingsBase {\n  asr_model: string | null\n  asr_provider: string | null\n  asr_prompt: string | null\n  llm_provider: string | null\n  llm_model: string | null\n  llm_temperature: number | null\n  transcription_prompt: string | null\n  editing_prompt: string | null\n  no_speech_threshold: number | null\n  low_quality_threshold: number | null\n}\n\nexport interface LlmSettings extends LlmSettingsBase {\n  id: string\n  created_at: Date\n  updated_at: Date\n  user_id: string\n}\n\nexport interface AdvancedSettings {\n  id: string\n  user_id: string\n  llm: LlmSettingsBase\n  created_at: Date\n  updated_at: Date\n}\n\nexport interface UserTrial {\n  user_id: string\n  trial_start_at: Date | null\n  trial_end_at: Date | null\n  has_completed_trial: boolean\n  stripe_subscription_id: string | null\n  created_at: Date\n  updated_at: Date\n}\n\nexport interface UserSubscription {\n  user_id: string\n  stripe_customer_id: string | null\n  stripe_subscription_id: string | null\n  subscription_start_at: Date | null\n  subscription_end_at: Date | null\n  created_at: Date\n  updated_at: Date\n}\n"
  },
  {
    "path": "server/src/db/repo.ts",
    "content": "import pool from '../db.js'\nimport {\n  Note,\n  Interaction,\n  DictionaryItem,\n  LlmSettings,\n  AdvancedSettings,\n  UserTrial,\n  UserSubscription,\n} from './models.js'\nimport {\n  CreateNoteRequest,\n  UpdateNoteRequest,\n  CreateInteractionRequest,\n  UpdateInteractionRequest,\n  CreateDictionaryItemRequest,\n  UpdateDictionaryItemRequest,\n  UpdateAdvancedSettingsRequest,\n} from '../generated/ito_pb.js'\n\nexport class NotesRepository {\n  static async create(\n    noteData: CreateNoteRequest & { userId: string },\n  ): Promise<Note> {\n    const res = await pool.query<Note>(\n      `INSERT INTO notes (id, user_id, interaction_id, content)\n       VALUES ($1, $2, $3, $4)\n       RETURNING *`,\n      [\n        noteData.id,\n        noteData.userId,\n        noteData.interactionId || null,\n        noteData.content,\n      ],\n    )\n    return res.rows[0]\n  }\n\n  static async findById(id: string): Promise<Note | undefined> {\n    const res = await pool.query<Note>('SELECT * FROM notes WHERE id = $1', [\n      id,\n    ])\n    return res.rows[0]\n  }\n\n  static async findByUserId(userId: string, since?: Date): Promise<Note[]> {\n    let query = 'SELECT * FROM notes WHERE user_id = $1'\n    const params: any[] = [userId]\n\n    if (since) {\n      query += ' AND (updated_at > $2 OR deleted_at > $2)'\n      params.push(since)\n    }\n\n    query += ' ORDER BY updated_at ASC'\n\n    const res = await pool.query<Note>(query, params)\n    return res.rows\n  }\n\n  static async update(noteData: UpdateNoteRequest): Promise<Note | undefined> {\n    const res = await pool.query<Note>(\n      `UPDATE notes\n       SET content = $1, updated_at = current_timestamp\n       WHERE id = $2\n       RETURNING *`,\n      [noteData.content, noteData.id],\n    )\n    return res.rows[0]\n  }\n\n  static async softDelete(id: string): Promise<boolean> {\n    const res = await pool.query(\n      `UPDATE notes\n       SET deleted_at = current_timestamp\n       WHERE id = $1`,\n      [id],\n    )\n    return (res.rowCount ?? 0) > 0\n  }\n\n  static async deleteAllUserData(userId: string): Promise<boolean> {\n    const res = await pool.query(\n      `UPDATE notes\n       SET deleted_at = current_timestamp\n       WHERE user_id = $1`,\n      [userId],\n    )\n    return (res.rowCount ?? 0) > 0\n  }\n\n  static async hardDeleteAllUserData(userId: string): Promise<number> {\n    const res = await pool.query('DELETE FROM notes WHERE user_id = $1', [\n      userId,\n    ])\n    return res.rowCount ?? 0\n  }\n}\n\nexport class InteractionsRepository {\n  static async create(\n    interactionData: Omit<CreateInteractionRequest, 'rawAudio'> & {\n      userId: string\n      rawAudioId?: string\n    },\n  ): Promise<Interaction> {\n    const res = await pool.query<Interaction>(\n      `INSERT INTO interactions (id, user_id, title, asr_output, llm_output, raw_audio_id, duration_ms)\n       VALUES ($1, $2, $3, $4, $5, $6, $7)\n       RETURNING *`,\n      [\n        interactionData.id,\n        interactionData.userId,\n        interactionData.title,\n        interactionData.asrOutput,\n        interactionData.llmOutput,\n        interactionData.rawAudioId || null,\n        interactionData.durationMs ?? 0,\n      ],\n    )\n    return res.rows[0]\n  }\n\n  static async findById(id: string): Promise<Interaction | undefined> {\n    const res = await pool.query<Interaction>(\n      'SELECT * FROM interactions WHERE id = $1 AND deleted_at IS NULL',\n      [id],\n    )\n    return res.rows[0]\n  }\n\n  static async findByUserId(\n    userId: string,\n    since?: Date,\n  ): Promise<Interaction[]> {\n    let query = 'SELECT * FROM interactions WHERE user_id = $1'\n    const params: any[] = [userId]\n\n    if (since) {\n      query += ' AND (updated_at > $2 OR deleted_at > $2)'\n      params.push(since)\n    }\n\n    query += ' ORDER BY updated_at ASC'\n\n    const res = await pool.query<Interaction>(query, params)\n    return res.rows\n  }\n\n  static async update(\n    interactionData: UpdateInteractionRequest,\n  ): Promise<Interaction | undefined> {\n    const res = await pool.query<Interaction>(\n      `UPDATE interactions\n       SET title = $1, updated_at = current_timestamp\n       WHERE id = $2 AND deleted_at IS NULL\n       RETURNING *`,\n      [interactionData.title, interactionData.id],\n    )\n    return res.rows[0]\n  }\n\n  static async softDelete(id: string): Promise<boolean> {\n    const res = await pool.query(\n      `UPDATE interactions\n       SET deleted_at = current_timestamp\n       WHERE id = $1`,\n      [id],\n    )\n    return (res.rowCount ?? 0) > 0\n  }\n\n  static async deleteAllUserData(userId: string): Promise<boolean> {\n    const res = await pool.query(\n      `UPDATE interactions\n       SET deleted_at = current_timestamp\n       WHERE user_id = $1`,\n      [userId],\n    )\n    return (res.rowCount ?? 0) > 0\n  }\n\n  static async hardDeleteAllUserData(userId: string): Promise<number> {\n    const res = await pool.query(\n      'DELETE FROM interactions WHERE user_id = $1',\n      [userId],\n    )\n    return res.rowCount ?? 0\n  }\n}\n\nexport class DictionaryRepository {\n  static async create(\n    itemData: CreateDictionaryItemRequest & { userId: string },\n  ): Promise<DictionaryItem> {\n    const res = await pool.query<DictionaryItem>(\n      `INSERT INTO dictionary_items (id, user_id, word, pronunciation)\n       VALUES ($1, $2, $3, $4)\n       RETURNING *`,\n      [itemData.id, itemData.userId, itemData.word, itemData.pronunciation],\n    )\n    return res.rows[0]\n  }\n\n  static async findByUserId(\n    userId: string,\n    since?: Date,\n  ): Promise<DictionaryItem[]> {\n    let query = 'SELECT * FROM dictionary_items WHERE user_id = $1'\n    const params: any[] = [userId]\n\n    if (since) {\n      query += ' AND (updated_at > $2 OR deleted_at > $2)'\n      params.push(since)\n    }\n\n    query += ' ORDER BY updated_at ASC'\n\n    const res = await pool.query<DictionaryItem>(query, params)\n    return res.rows\n  }\n\n  static async update(\n    itemData: UpdateDictionaryItemRequest,\n  ): Promise<DictionaryItem | undefined> {\n    const res = await pool.query<DictionaryItem>(\n      `UPDATE dictionary_items\n       SET word = $1, pronunciation = $2, updated_at = current_timestamp\n       WHERE id = $3 AND deleted_at IS NULL\n       RETURNING *`,\n      [itemData.word, itemData.pronunciation, itemData.id],\n    )\n    return res.rows[0]\n  }\n\n  static async softDelete(id: string): Promise<boolean> {\n    const res = await pool.query(\n      `UPDATE dictionary_items\n       SET deleted_at = current_timestamp\n       WHERE id = $1`,\n      [id],\n    )\n    return (res.rowCount ?? 0) > 0\n  }\n\n  static async deleteAllUserData(userId: string): Promise<boolean> {\n    const res = await pool.query(\n      `UPDATE dictionary_items\n       SET deleted_at = current_timestamp\n       WHERE user_id = $1`,\n      [userId],\n    )\n    return (res.rowCount ?? 0) > 0\n  }\n\n  static async hardDeleteAllUserData(userId: string): Promise<number> {\n    const res = await pool.query(\n      'DELETE FROM dictionary_items WHERE user_id = $1',\n      [userId],\n    )\n    return res.rowCount ?? 0\n  }\n}\n\nexport class AdvancedSettingsRepository {\n  static async findByUserId(\n    userId: string,\n  ): Promise<AdvancedSettings | undefined> {\n    const res = await pool.query<LlmSettings>(\n      'SELECT * FROM llm_settings WHERE user_id = $1',\n      [userId],\n    )\n\n    if (res.rows.length === 0) {\n      return undefined\n    }\n\n    const llmSettings = res.rows[0]\n    return {\n      id: llmSettings.id,\n      user_id: llmSettings.user_id,\n      llm: {\n        asr_model: llmSettings.asr_model,\n        asr_provider: llmSettings.asr_provider,\n        asr_prompt: llmSettings.asr_prompt,\n        llm_provider: llmSettings.llm_provider,\n        llm_model: llmSettings.llm_model,\n        llm_temperature: llmSettings.llm_temperature,\n        transcription_prompt: llmSettings.transcription_prompt,\n        editing_prompt: llmSettings.editing_prompt,\n        no_speech_threshold: llmSettings.no_speech_threshold,\n        low_quality_threshold: llmSettings.low_quality_threshold,\n      },\n      created_at: llmSettings.created_at,\n      updated_at: llmSettings.updated_at,\n    }\n  }\n\n  static async upsert(\n    userId: string,\n    settingsData: UpdateAdvancedSettingsRequest,\n  ): Promise<AdvancedSettings> {\n    const res = await pool.query<LlmSettings>(\n      `INSERT INTO llm_settings (\n         user_id, asr_model, asr_provider, asr_prompt, llm_provider, llm_model, \n         llm_temperature, transcription_prompt, editing_prompt, no_speech_threshold, \n         low_quality_threshold, updated_at\n       )\n       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, current_timestamp)\n       ON CONFLICT (user_id)\n       DO UPDATE SET\n         asr_model = EXCLUDED.asr_model,\n         asr_provider = EXCLUDED.asr_provider,\n         asr_prompt = EXCLUDED.asr_prompt,\n         llm_provider = EXCLUDED.llm_provider,\n         llm_model = EXCLUDED.llm_model,\n         llm_temperature = EXCLUDED.llm_temperature,\n         transcription_prompt = EXCLUDED.transcription_prompt,\n         editing_prompt = EXCLUDED.editing_prompt,\n         no_speech_threshold = EXCLUDED.no_speech_threshold,\n         low_quality_threshold = EXCLUDED.low_quality_threshold,\n         updated_at = current_timestamp\n       RETURNING *`,\n      [\n        userId,\n        settingsData.llm?.asrModel || 'whisper-large-v3',\n        settingsData.llm?.asrProvider || '',\n        settingsData.llm?.asrPrompt || '',\n        settingsData.llm?.llmProvider || '',\n        settingsData.llm?.llmModel || '',\n        settingsData.llm?.llmTemperature || 0.0,\n        settingsData.llm?.transcriptionPrompt || '',\n        settingsData.llm?.editingPrompt || '',\n        settingsData.llm?.noSpeechThreshold || 0.0,\n        settingsData.llm?.lowQualityThreshold || 0.0,\n      ],\n    )\n\n    const llmSettings = res.rows[0]\n    console.log('Upserted advanced settings:', llmSettings)\n    return {\n      id: llmSettings.id,\n      user_id: llmSettings.user_id,\n      llm: {\n        asr_model: llmSettings.asr_model,\n        asr_provider: llmSettings.asr_provider,\n        asr_prompt: llmSettings.asr_prompt,\n        llm_provider: llmSettings.llm_provider,\n        llm_model: llmSettings.llm_model,\n        llm_temperature: llmSettings.llm_temperature,\n        transcription_prompt: llmSettings.transcription_prompt,\n        editing_prompt: llmSettings.editing_prompt,\n        no_speech_threshold: llmSettings.no_speech_threshold,\n        low_quality_threshold: llmSettings.low_quality_threshold,\n      },\n      created_at: llmSettings.created_at,\n      updated_at: llmSettings.updated_at,\n    }\n  }\n\n  static async hardDeleteByUserId(userId: string): Promise<number> {\n    const res = await pool.query(\n      'DELETE FROM llm_settings WHERE user_id = $1',\n      [userId],\n    )\n    return res.rowCount ?? 0\n  }\n}\nexport class IpLinkRepository {\n  static async cleanupExpired(): Promise<number> {\n    const res = await pool.query(\n      'DELETE FROM ip_link_candidates WHERE expires_at < NOW()',\n    )\n    return res.rowCount ?? 0\n  }\n\n  static async registerCandidate(\n    ipHash: string,\n    websiteDistinctId: string,\n  ): Promise<void> {\n    await this.cleanupExpired()\n    const expiresAt = new Date(Date.now() + 60 * 60 * 1000) // 1 hour\n    await pool.query(\n      `INSERT INTO ip_link_candidates (ip_hash, website_distinct_id, expires_at)\n       VALUES ($1, $2, $3)`,\n      [ipHash, websiteDistinctId, expiresAt],\n    )\n  }\n  static async consumeLatestForIp(ipHash: string): Promise<string | null> {\n    await this.cleanupExpired()\n    const res = await pool.query<{ website_distinct_id: string }>(\n      `DELETE FROM ip_link_candidates\n       WHERE ctid IN (\n         SELECT ctid FROM ip_link_candidates\n         WHERE ip_hash = $1 AND expires_at > NOW()\n         ORDER BY expires_at DESC\n         LIMIT 1\n       )\n       RETURNING website_distinct_id`,\n      [ipHash],\n    )\n    return res.rows[0]?.website_distinct_id ?? null\n  }\n}\n\nexport class TrialsRepository {\n  static async getByUserId(userId: string): Promise<UserTrial | undefined> {\n    const res = await pool.query<UserTrial>(\n      'SELECT * FROM user_trials WHERE user_id = $1',\n      [userId],\n    )\n    return res.rows[0]\n  }\n\n  static async getByStripeSubscriptionId(\n    stripeSubscriptionId: string,\n  ): Promise<UserTrial | undefined> {\n    const res = await pool.query<UserTrial>(\n      'SELECT * FROM user_trials WHERE stripe_subscription_id = $1',\n      [stripeSubscriptionId],\n    )\n    return res.rows[0]\n  }\n\n  static async upsertFromStripeSubscription(\n    userId: string,\n    stripeSubscriptionId: string,\n    trialStartAt: Date | null,\n    hasCompletedTrial: boolean,\n    trialEndAt?: Date | null,\n  ): Promise<UserTrial> {\n    const res = await pool.query<UserTrial>(\n      `INSERT INTO user_trials (\n         user_id, stripe_subscription_id, trial_start_at, trial_end_at, has_completed_trial, updated_at\n       ) VALUES ($1, $2, $3, $4, $5, current_timestamp)\n       ON CONFLICT (user_id)\n       DO UPDATE SET\n         stripe_subscription_id = EXCLUDED.stripe_subscription_id,\n         trial_start_at = EXCLUDED.trial_start_at,\n         trial_end_at = EXCLUDED.trial_end_at,\n         has_completed_trial = EXCLUDED.has_completed_trial,\n         updated_at = current_timestamp\n       RETURNING *`,\n      [\n        userId,\n        stripeSubscriptionId,\n        trialStartAt,\n        trialEndAt ?? null,\n        hasCompletedTrial,\n      ],\n    )\n    return res.rows[0]\n  }\n\n  static async startTrial(userId: string, startAt?: Date): Promise<UserTrial> {\n    // Ensure a row exists; idempotently set start when not completed and not already set\n    const existing = await this.getByUserId(userId)\n    if (!existing) {\n      const res = await pool.query<UserTrial>(\n        `INSERT INTO user_trials (user_id, trial_start_at, has_completed_trial)\n         VALUES ($1, $2, false)\n         RETURNING *`,\n        [userId, startAt ?? new Date()],\n      )\n      return res.rows[0]\n    }\n\n    if (existing.has_completed_trial) {\n      return existing\n    }\n\n    if (existing.trial_start_at == null) {\n      const res = await pool.query<UserTrial>(\n        `UPDATE user_trials\n         SET trial_start_at = $2, updated_at = current_timestamp\n         WHERE user_id = $1\n         RETURNING *`,\n        [userId, startAt ?? new Date()],\n      )\n      return res.rows[0]\n    }\n\n    return existing\n  }\n\n  static async completeTrial(userId: string): Promise<UserTrial> {\n    const res = await pool.query<UserTrial>(\n      `UPDATE user_trials\n       SET has_completed_trial = true,\n           trial_start_at = NULL,\n           updated_at = current_timestamp\n       WHERE user_id = $1\n       RETURNING *`,\n      [userId],\n    )\n\n    if (res.rows[0]) return res.rows[0]\n\n    const insert = await pool.query<UserTrial>(\n      `INSERT INTO user_trials (user_id, has_completed_trial)\n       VALUES ($1, true)\n       RETURNING *`,\n      [userId],\n    )\n    return insert.rows[0]\n  }\n}\n\nexport class SubscriptionsRepository {\n  static async getByUserId(\n    userId: string,\n  ): Promise<UserSubscription | undefined> {\n    const res = await pool.query<UserSubscription>(\n      'SELECT * FROM user_subscriptions WHERE user_id = $1',\n      [userId],\n    )\n    return res.rows[0]\n  }\n\n  static async upsertActive(\n    userId: string,\n    stripeCustomerId: string | null,\n    stripeSubscriptionId: string | null,\n    startAt: Date | null,\n    endAt?: Date | null,\n  ): Promise<UserSubscription> {\n    const res = await pool.query<UserSubscription>(\n      `INSERT INTO user_subscriptions (\n         user_id, stripe_customer_id, stripe_subscription_id, subscription_start_at, subscription_end_at, updated_at\n       ) VALUES ($1, $2, $3, $4, $5, current_timestamp)\n       ON CONFLICT (user_id)\n       DO UPDATE SET\n         stripe_customer_id = EXCLUDED.stripe_customer_id,\n         stripe_subscription_id = EXCLUDED.stripe_subscription_id,\n         subscription_start_at = EXCLUDED.subscription_start_at,\n         subscription_end_at = EXCLUDED.subscription_end_at,\n         updated_at = current_timestamp\n       RETURNING *`,\n      [userId, stripeCustomerId, stripeSubscriptionId, startAt, endAt ?? null],\n    )\n    return res.rows[0]\n  }\n\n  static async updateSubscriptionEndAt(\n    userId: string,\n    endAt: Date | null,\n  ): Promise<UserSubscription> {\n    const res = await pool.query<UserSubscription>(\n      `UPDATE user_subscriptions\n       SET subscription_end_at = $2, updated_at = current_timestamp\n       WHERE user_id = $1\n       RETURNING *`,\n      [userId, endAt],\n    )\n    return res.rows[0]\n  }\n\n  static async deleteByStripeSubscriptionId(\n    stripeSubscriptionId: string,\n  ): Promise<boolean> {\n    const res = await pool.query(\n      'DELETE FROM user_subscriptions WHERE stripe_subscription_id = $1',\n      [stripeSubscriptionId],\n    )\n    return (res.rowCount ?? 0) > 0\n  }\n}\n"
  },
  {
    "path": "server/src/db.ts",
    "content": "import { Pool } from 'pg'\n\nimport dotenv from 'dotenv'\ndotenv.config()\n\nconst pool = new Pool({\n  host: process.env.DB_HOST,\n  port: Number(process.env.DB_PORT ?? 5432),\n  user: process.env.DB_USER,\n  password: process.env.DB_PASS,\n  database: process.env.DB_NAME,\n})\n\nexport default pool\n"
  },
  {
    "path": "server/src/generated/buf/validate/validate_pb.ts",
    "content": "// Copyright 2023-2025 Buf Technologies, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//      http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// @generated by protoc-gen-es v2.7.0 with parameter \"target=ts,import_extension=.js\"\n// @generated from file buf/validate/validate.proto (package buf.validate, syntax proto2)\n/* eslint-disable */\n\nimport type { GenEnum, GenExtension, GenFile, GenMessage } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, extDesc, fileDesc, messageDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { Duration, FieldDescriptorProto_Type, FieldOptions, MessageOptions, OneofOptions, Timestamp } from \"@bufbuild/protobuf/wkt\";\nimport { file_google_protobuf_descriptor, file_google_protobuf_duration, file_google_protobuf_timestamp } from \"@bufbuild/protobuf/wkt\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file buf/validate/validate.proto.\n */\nexport const file_buf_validate_validate: GenFile = /*@__PURE__*/\n  fileDesc(\"ChtidWYvdmFsaWRhdGUvdmFsaWRhdGUucHJvdG8SDGJ1Zi52YWxpZGF0ZSI3CgRSdWxlEgoKAmlkGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSEgoKZXhwcmVzc2lvbhgDIAEoCSJuCgxNZXNzYWdlUnVsZXMSHwoDY2VsGAMgAygLMhIuYnVmLnZhbGlkYXRlLlJ1bGUSLQoFb25lb2YYBCADKAsyHi5idWYudmFsaWRhdGUuTWVzc2FnZU9uZW9mUnVsZUoECAEQAlIIZGlzYWJsZWQiNAoQTWVzc2FnZU9uZW9mUnVsZRIOCgZmaWVsZHMYASADKAkSEAoIcmVxdWlyZWQYAiABKAgiHgoKT25lb2ZSdWxlcxIQCghyZXF1aXJlZBgBIAEoCCK/CAoKRmllbGRSdWxlcxIfCgNjZWwYFyADKAsyEi5idWYudmFsaWRhdGUuUnVsZRIQCghyZXF1aXJlZBgZIAEoCBIkCgZpZ25vcmUYGyABKA4yFC5idWYudmFsaWRhdGUuSWdub3JlEikKBWZsb2F0GAEgASgLMhguYnVmLnZhbGlkYXRlLkZsb2F0UnVsZXNIABIrCgZkb3VibGUYAiABKAsyGS5idWYudmFsaWRhdGUuRG91YmxlUnVsZXNIABIpCgVpbnQzMhgDIAEoCzIYLmJ1Zi52YWxpZGF0ZS5JbnQzMlJ1bGVzSAASKQoFaW50NjQYBCABKAsyGC5idWYudmFsaWRhdGUuSW50NjRSdWxlc0gAEisKBnVpbnQzMhgFIAEoCzIZLmJ1Zi52YWxpZGF0ZS5VSW50MzJSdWxlc0gAEisKBnVpbnQ2NBgGIAEoCzIZLmJ1Zi52YWxpZGF0ZS5VSW50NjRSdWxlc0gAEisKBnNpbnQzMhgHIAEoCzIZLmJ1Zi52YWxpZGF0ZS5TSW50MzJSdWxlc0gAEisKBnNpbnQ2NBgIIAEoCzIZLmJ1Zi52YWxpZGF0ZS5TSW50NjRSdWxlc0gAEi0KB2ZpeGVkMzIYCSABKAsyGi5idWYudmFsaWRhdGUuRml4ZWQzMlJ1bGVzSAASLQoHZml4ZWQ2NBgKIAEoCzIaLmJ1Zi52YWxpZGF0ZS5GaXhlZDY0UnVsZXNIABIvCghzZml4ZWQzMhgLIAEoCzIbLmJ1Zi52YWxpZGF0ZS5TRml4ZWQzMlJ1bGVzSAASLwoIc2ZpeGVkNjQYDCABKAsyGy5idWYudmFsaWRhdGUuU0ZpeGVkNjRSdWxlc0gAEicKBGJvb2wYDSABKAsyFy5idWYudmFsaWRhdGUuQm9vbFJ1bGVzSAASKwoGc3RyaW5nGA4gASgLMhkuYnVmLnZhbGlkYXRlLlN0cmluZ1J1bGVzSAASKQoFYnl0ZXMYDyABKAsyGC5idWYudmFsaWRhdGUuQnl0ZXNSdWxlc0gAEicKBGVudW0YECABKAsyFy5idWYudmFsaWRhdGUuRW51bVJ1bGVzSAASLwoIcmVwZWF0ZWQYEiABKAsyGy5idWYudmFsaWRhdGUuUmVwZWF0ZWRSdWxlc0gAEiUKA21hcBgTIAEoCzIWLmJ1Zi52YWxpZGF0ZS5NYXBSdWxlc0gAEiUKA2FueRgUIAEoCzIWLmJ1Zi52YWxpZGF0ZS5BbnlSdWxlc0gAEi8KCGR1cmF0aW9uGBUgASgLMhsuYnVmLnZhbGlkYXRlLkR1cmF0aW9uUnVsZXNIABIxCgl0aW1lc3RhbXAYFiABKAsyHC5idWYudmFsaWRhdGUuVGltZXN0YW1wUnVsZXNIAEIGCgR0eXBlSgQIGBAZSgQIGhAbUgdza2lwcGVkUgxpZ25vcmVfZW1wdHkiVQoPUHJlZGVmaW5lZFJ1bGVzEh8KA2NlbBgBIAMoCzISLmJ1Zi52YWxpZGF0ZS5SdWxlSgQIGBAZSgQIGhAbUgdza2lwcGVkUgxpZ25vcmVfZW1wdHki2hcKCkZsb2F0UnVsZXMSgwEKBWNvbnN0GAEgASgCQnTCSHEKbwoLZmxvYXQuY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKfAQoCbHQYAiABKAJCkAHCSIwBCokBCghmbG9hdC5sdBp9IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA+PSBydWxlcy5sdCk/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKvAQoDbHRlGAMgASgCQp8BwkibAQqYAQoJZmxvYXQubHRlGooBIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA+IHJ1bGVzLmx0ZSk/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAAS7wcKAmd0GAQgASgCQuAHwkjcBwqNAQoIZmxvYXQuZ3QagAEhaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwrDAQoLZmxvYXQuZ3RfbHQaswFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrNAQoVZmxvYXQuZ3RfbHRfZXhjbHVzaXZlGrMBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmICh0aGlzLmlzTmFuKCkgfHwgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCkpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycK0wEKDGZsb2F0Lmd0X2x0ZRrCAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCt0BChZmbG9hdC5ndF9sdGVfZXhjbHVzaXZlGsIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHRoaXMuaXNOYW4oKSB8fCAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARK6CAoDZ3RlGAUgASgCQqoIwkimCAqbAQoJZmxvYXQuZ3RlGo0BIWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCtIBCgxmbG9hdC5ndGVfbHQawQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtwBChZmbG9hdC5ndGVfbHRfZXhjbHVzaXZlGsEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ZSAmJiAodGhpcy5pc05hbigpIHx8IChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwriAQoNZmxvYXQuZ3RlX2x0ZRrQAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJycK7AEKF2Zsb2F0Lmd0ZV9sdGVfZXhjbHVzaXZlGtABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmICh0aGlzLmlzTmFuKCkgfHwgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSkpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEn8KAmluGAYgAygCQnPCSHAKbgoIZmxvYXQuaW4aYiEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ3ZhbHVlIG11c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEnYKBm5vdF9pbhgHIAMoAkJmwkhjCmEKDGZsb2F0Lm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEnUKBmZpbml0ZRgIIAEoCEJlwkhiCmAKDGZsb2F0LmZpbml0ZRpQcnVsZXMuZmluaXRlID8gKHRoaXMuaXNOYW4oKSB8fCB0aGlzLmlzSW5mKCkgPyAndmFsdWUgbXVzdCBiZSBmaW5pdGUnIDogJycpIDogJycSKwoHZXhhbXBsZRgJIAMoAkIawkgXChUKDWZsb2F0LmV4YW1wbGUaBHRydWUqCQjoBxCAgICAAkILCglsZXNzX3RoYW5CDgoMZ3JlYXRlcl90aGFuIu0XCgtEb3VibGVSdWxlcxKEAQoFY29uc3QYASABKAFCdcJIcgpwCgxkb3VibGUuY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKgAQoCbHQYAiABKAFCkQHCSI0BCooBCglkb3VibGUubHQafSFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPj0gcnVsZXMubHQpPyAndmFsdWUgbXVzdCBiZSBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMubHRdKSA6ICcnSAASsAEKA2x0ZRgDIAEoAUKgAcJInAEKmQEKCmRvdWJsZS5sdGUaigEhaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzID4gcnVsZXMubHRlKT8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABL0BwoCZ3QYBCABKAFC5QfCSOEHCo4BCglkb3VibGUuZ3QagAEhaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwrEAQoMZG91YmxlLmd0X2x0GrMBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndCAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKzgEKFmRvdWJsZS5ndF9sdF9leGNsdXNpdmUaswFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHRoaXMuaXNOYW4oKSB8fCAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrUAQoNZG91YmxlLmd0X2x0ZRrCAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCt4BChdkb3VibGUuZ3RfbHRlX2V4Y2x1c2l2ZRrCAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmICh0aGlzLmlzTmFuKCkgfHwgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCkpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAESvwgKA2d0ZRgFIAEoAUKvCMJIqwgKnAEKCmRvdWJsZS5ndGUajQEhaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZV0pIDogJycK0wEKDWRvdWJsZS5ndGVfbHQawQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCt0BChdkb3VibGUuZ3RlX2x0X2V4Y2x1c2l2ZRrBAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHRoaXMuaXNOYW4oKSB8fCAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycK4wEKDmRvdWJsZS5ndGVfbHRlGtABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrtAQoYZG91YmxlLmd0ZV9sdGVfZXhjbHVzaXZlGtABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmICh0aGlzLmlzTmFuKCkgfHwgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSkpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEoABCgJpbhgGIAMoAUJ0wkhxCm8KCWRvdWJsZS5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSdwoGbm90X2luGAcgAygBQmfCSGQKYgoNZG91YmxlLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEnYKBmZpbml0ZRgIIAEoCEJmwkhjCmEKDWRvdWJsZS5maW5pdGUaUHJ1bGVzLmZpbml0ZSA/ICh0aGlzLmlzTmFuKCkgfHwgdGhpcy5pc0luZigpID8gJ3ZhbHVlIG11c3QgYmUgZmluaXRlJyA6ICcnKSA6ICcnEiwKB2V4YW1wbGUYCSADKAFCG8JIGAoWCg5kb3VibGUuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4ijBUKCkludDMyUnVsZXMSgwEKBWNvbnN0GAEgASgFQnTCSHEKbwoLaW50MzIuY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKKAQoCbHQYAiABKAVCfMJIeQp3CghpbnQzMi5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKcAQoDbHRlGAMgASgFQowBwkiIAQqFAQoJaW50MzIubHRlGnghaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+IHJ1bGVzLmx0ZT8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABKXBwoCZ3QYBCABKAVCiAfCSIQHCnoKCGludDMyLmd0Gm4haGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwqzAQoLaW50MzIuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrsBChVpbnQzMi5ndF9sdF9leGNsdXNpdmUaoQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrDAQoMaW50MzIuZ3RfbHRlGrIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrLAQoWaW50MzIuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES4wcKA2d0ZRgFIAEoBULTB8JIzwcKiAEKCWludDMyLmd0ZRp7IWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPCBydWxlcy5ndGU/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCsIBCgxpbnQzMi5ndGVfbHQasQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKygEKFmludDMyLmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtIBCg1pbnQzMi5ndGVfbHRlGsABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnCtoBChdpbnQzMi5ndGVfbHRlX2V4Y2x1c2l2ZRq+AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARJ/CgJpbhgGIAMoBUJzwkhwCm4KCGludDMyLmluGmIhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ2CgZub3RfaW4YByADKAVCZsJIYwphCgxpbnQzMi5ub3RfaW4aUXRoaXMgaW4gcnVsZXMubm90X2luID8gJ3ZhbHVlIG11c3Qgbm90IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbcnVsZXMubm90X2luXSkgOiAnJxIrCgdleGFtcGxlGAggAygFQhrCSBcKFQoNaW50MzIuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4ijBUKCkludDY0UnVsZXMSgwEKBWNvbnN0GAEgASgDQnTCSHEKbwoLaW50NjQuY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKKAQoCbHQYAiABKANCfMJIeQp3CghpbnQ2NC5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKcAQoDbHRlGAMgASgDQowBwkiIAQqFAQoJaW50NjQubHRlGnghaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+IHJ1bGVzLmx0ZT8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABKXBwoCZ3QYBCABKANCiAfCSIQHCnoKCGludDY0Lmd0Gm4haGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwqzAQoLaW50NjQuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrsBChVpbnQ2NC5ndF9sdF9leGNsdXNpdmUaoQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrDAQoMaW50NjQuZ3RfbHRlGrIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrLAQoWaW50NjQuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES4wcKA2d0ZRgFIAEoA0LTB8JIzwcKiAEKCWludDY0Lmd0ZRp7IWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPCBydWxlcy5ndGU/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCsIBCgxpbnQ2NC5ndGVfbHQasQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKygEKFmludDY0Lmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtIBCg1pbnQ2NC5ndGVfbHRlGsABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnCtoBChdpbnQ2NC5ndGVfbHRlX2V4Y2x1c2l2ZRq+AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARJ/CgJpbhgGIAMoA0JzwkhwCm4KCGludDY0LmluGmIhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ2CgZub3RfaW4YByADKANCZsJIYwphCgxpbnQ2NC5ub3RfaW4aUXRoaXMgaW4gcnVsZXMubm90X2luID8gJ3ZhbHVlIG11c3Qgbm90IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbcnVsZXMubm90X2luXSkgOiAnJxIrCgdleGFtcGxlGAkgAygDQhrCSBcKFQoNaW50NjQuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4inhUKC1VJbnQzMlJ1bGVzEoQBCgVjb25zdBgBIAEoDUJ1wkhyCnAKDHVpbnQzMi5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEosBCgJsdBgCIAEoDUJ9wkh6CngKCXVpbnQzMi5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKdAQoDbHRlGAMgASgNQo0BwkiJAQqGAQoKdWludDMyLmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASnAcKAmd0GAQgASgNQo0HwkiJBwp7Cgl1aW50MzIuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrQBCgx1aW50MzIuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrwBChZ1aW50MzIuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxAEKDXVpbnQzMi5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCswBChd1aW50MzIuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES6AcKA2d0ZRgFIAEoDULYB8JI1AcKiQEKCnVpbnQzMi5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrDAQoNdWludDMyLmd0ZV9sdBqxAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrLAQoXdWludDMyLmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtMBCg51aW50MzIuZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrbAQoYdWludDMyLmd0ZV9sdGVfZXhjbHVzaXZlGr4BaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEoABCgJpbhgGIAMoDUJ0wkhxCm8KCXVpbnQzMi5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSdwoGbm90X2luGAcgAygNQmfCSGQKYgoNdWludDMyLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEiwKB2V4YW1wbGUYCCADKA1CG8JIGAoWCg51aW50MzIuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4inhUKC1VJbnQ2NFJ1bGVzEoQBCgVjb25zdBgBIAEoBEJ1wkhyCnAKDHVpbnQ2NC5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEosBCgJsdBgCIAEoBEJ9wkh6CngKCXVpbnQ2NC5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKdAQoDbHRlGAMgASgEQo0BwkiJAQqGAQoKdWludDY0Lmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASnAcKAmd0GAQgASgEQo0HwkiJBwp7Cgl1aW50NjQuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrQBCgx1aW50NjQuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrwBChZ1aW50NjQuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxAEKDXVpbnQ2NC5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCswBChd1aW50NjQuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES6AcKA2d0ZRgFIAEoBELYB8JI1AcKiQEKCnVpbnQ2NC5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrDAQoNdWludDY0Lmd0ZV9sdBqxAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrLAQoXdWludDY0Lmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtMBCg51aW50NjQuZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrbAQoYdWludDY0Lmd0ZV9sdGVfZXhjbHVzaXZlGr4BaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEoABCgJpbhgGIAMoBEJ0wkhxCm8KCXVpbnQ2NC5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSdwoGbm90X2luGAcgAygEQmfCSGQKYgoNdWludDY0Lm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEiwKB2V4YW1wbGUYCCADKARCG8JIGAoWCg51aW50NjQuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4inhUKC1NJbnQzMlJ1bGVzEoQBCgVjb25zdBgBIAEoEUJ1wkhyCnAKDHNpbnQzMi5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEosBCgJsdBgCIAEoEUJ9wkh6CngKCXNpbnQzMi5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKdAQoDbHRlGAMgASgRQo0BwkiJAQqGAQoKc2ludDMyLmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASnAcKAmd0GAQgASgRQo0HwkiJBwp7CglzaW50MzIuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrQBCgxzaW50MzIuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrwBChZzaW50MzIuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxAEKDXNpbnQzMi5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCswBChdzaW50MzIuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES6AcKA2d0ZRgFIAEoEULYB8JI1AcKiQEKCnNpbnQzMi5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrDAQoNc2ludDMyLmd0ZV9sdBqxAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrLAQoXc2ludDMyLmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtMBCg5zaW50MzIuZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrbAQoYc2ludDMyLmd0ZV9sdGVfZXhjbHVzaXZlGr4BaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEoABCgJpbhgGIAMoEUJ0wkhxCm8KCXNpbnQzMi5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSdwoGbm90X2luGAcgAygRQmfCSGQKYgoNc2ludDMyLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEiwKB2V4YW1wbGUYCCADKBFCG8JIGAoWCg5zaW50MzIuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4inhUKC1NJbnQ2NFJ1bGVzEoQBCgVjb25zdBgBIAEoEkJ1wkhyCnAKDHNpbnQ2NC5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEosBCgJsdBgCIAEoEkJ9wkh6CngKCXNpbnQ2NC5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKdAQoDbHRlGAMgASgSQo0BwkiJAQqGAQoKc2ludDY0Lmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASnAcKAmd0GAQgASgSQo0HwkiJBwp7CglzaW50NjQuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrQBCgxzaW50NjQuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrwBChZzaW50NjQuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxAEKDXNpbnQ2NC5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCswBChdzaW50NjQuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES6AcKA2d0ZRgFIAEoEkLYB8JI1AcKiQEKCnNpbnQ2NC5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrDAQoNc2ludDY0Lmd0ZV9sdBqxAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrLAQoXc2ludDY0Lmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtMBCg5zaW50NjQuZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrbAQoYc2ludDY0Lmd0ZV9sdGVfZXhjbHVzaXZlGr4BaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEoABCgJpbhgGIAMoEkJ0wkhxCm8KCXNpbnQ2NC5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSdwoGbm90X2luGAcgAygSQmfCSGQKYgoNc2ludDY0Lm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEiwKB2V4YW1wbGUYCCADKBJCG8JIGAoWCg5zaW50NjQuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4irxUKDEZpeGVkMzJSdWxlcxKFAQoFY29uc3QYASABKAdCdsJIcwpxCg1maXhlZDMyLmNvbnN0GmB0aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICd2YWx1ZSBtdXN0IGVxdWFsICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSjAEKAmx0GAIgASgHQn7CSHsKeQoKZml4ZWQzMi5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKeAQoDbHRlGAMgASgHQo4BwkiKAQqHAQoLZml4ZWQzMi5sdGUaeCFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID4gcnVsZXMubHRlPyAndmFsdWUgbXVzdCBiZSBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMubHRlXSkgOiAnJ0gAEqEHCgJndBgEIAEoB0KSB8JIjgcKfAoKZml4ZWQzMi5ndBpuIWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPD0gcnVsZXMuZ3Q/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndF0pIDogJycKtQEKDWZpeGVkMzIuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCr0BChdmaXhlZDMyLmd0X2x0X2V4Y2x1c2l2ZRqhAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndCAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCsUBCg5maXhlZDMyLmd0X2x0ZRqyAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJycKzQEKGGZpeGVkMzIuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES7QcKA2d0ZRgFIAEoB0LdB8JI2QcKigEKC2ZpeGVkMzIuZ3RlGnshaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8IHJ1bGVzLmd0ZT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZV0pIDogJycKxAEKDmZpeGVkMzIuZ3RlX2x0GrEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCswBChhmaXhlZDMyLmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtQBCg9maXhlZDMyLmd0ZV9sdGUawAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3RlICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJycK3AEKGWZpeGVkMzIuZ3RlX2x0ZV9leGNsdXNpdmUavgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnSAESgQEKAmluGAYgAygHQnXCSHIKcAoKZml4ZWQzMi5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSeAoGbm90X2luGAcgAygHQmjCSGUKYwoOZml4ZWQzMi5ub3RfaW4aUXRoaXMgaW4gcnVsZXMubm90X2luID8gJ3ZhbHVlIG11c3Qgbm90IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbcnVsZXMubm90X2luXSkgOiAnJxItCgdleGFtcGxlGAggAygHQhzCSBkKFwoPZml4ZWQzMi5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiKvFQoMRml4ZWQ2NFJ1bGVzEoUBCgVjb25zdBgBIAEoBkJ2wkhzCnEKDWZpeGVkNjQuY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKMAQoCbHQYAiABKAZCfsJIewp5CgpmaXhlZDY0Lmx0GmshaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAEp4BCgNsdGUYAyABKAZCjgHCSIoBCocBCgtmaXhlZDY0Lmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASoQcKAmd0GAQgASgGQpIHwkiOBwp8CgpmaXhlZDY0Lmd0Gm4haGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwq1AQoNZml4ZWQ2NC5ndF9sdBqjAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKvQEKF2ZpeGVkNjQuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxQEKDmZpeGVkNjQuZ3RfbHRlGrIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrNAQoYZml4ZWQ2NC5ndF9sdGVfZXhjbHVzaXZlGrABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARLtBwoDZ3RlGAUgASgGQt0HwkjZBwqKAQoLZml4ZWQ2NC5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrEAQoOZml4ZWQ2NC5ndGVfbHQasQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKzAEKGGZpeGVkNjQuZ3RlX2x0X2V4Y2x1c2l2ZRqvAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycK1AEKD2ZpeGVkNjQuZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrcAQoZZml4ZWQ2NC5ndGVfbHRlX2V4Y2x1c2l2ZRq+AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARKBAQoCaW4YBiADKAZCdcJIcgpwCgpmaXhlZDY0LmluGmIhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ4CgZub3RfaW4YByADKAZCaMJIZQpjCg5maXhlZDY0Lm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEi0KB2V4YW1wbGUYCCADKAZCHMJIGQoXCg9maXhlZDY0LmV4YW1wbGUaBHRydWUqCQjoBxCAgICAAkILCglsZXNzX3RoYW5CDgoMZ3JlYXRlcl90aGFuIsAVCg1TRml4ZWQzMlJ1bGVzEoYBCgVjb25zdBgBIAEoD0J3wkh0CnIKDnNmaXhlZDMyLmNvbnN0GmB0aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICd2YWx1ZSBtdXN0IGVxdWFsICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSjQEKAmx0GAIgASgPQn/CSHwKegoLc2ZpeGVkMzIubHQaayFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID49IHJ1bGVzLmx0PyAndmFsdWUgbXVzdCBiZSBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMubHRdKSA6ICcnSAASnwEKA2x0ZRgDIAEoD0KPAcJIiwEKiAEKDHNmaXhlZDMyLmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASpgcKAmd0GAQgASgPQpcHwkiTBwp9CgtzZml4ZWQzMi5ndBpuIWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPD0gcnVsZXMuZ3Q/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndF0pIDogJycKtgEKDnNmaXhlZDMyLmd0X2x0GqMBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndCAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwq+AQoYc2ZpeGVkMzIuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxgEKD3NmaXhlZDMyLmd0X2x0ZRqyAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJycKzgEKGXNmaXhlZDMyLmd0X2x0ZV9leGNsdXNpdmUasAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndCAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJ0gBEvIHCgNndGUYBSABKA9C4gfCSN4HCosBCgxzZml4ZWQzMi5ndGUaeyFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwrFAQoPc2ZpeGVkMzIuZ3RlX2x0GrEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCs0BChlzZml4ZWQzMi5ndGVfbHRfZXhjbHVzaXZlGq8BaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrVAQoQc2ZpeGVkMzIuZ3RlX2x0ZRrAAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrdAQoac2ZpeGVkMzIuZ3RlX2x0ZV9leGNsdXNpdmUavgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnSAESggEKAmluGAYgAygPQnbCSHMKcQoLc2ZpeGVkMzIuaW4aYiEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ3ZhbHVlIG11c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEnkKBm5vdF9pbhgHIAMoD0JpwkhmCmQKD3NmaXhlZDMyLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEi4KB2V4YW1wbGUYCCADKA9CHcJIGgoYChBzZml4ZWQzMi5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiLAFQoNU0ZpeGVkNjRSdWxlcxKGAQoFY29uc3QYASABKBBCd8JIdApyCg5zZml4ZWQ2NC5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEo0BCgJsdBgCIAEoEEJ/wkh8CnoKC3NmaXhlZDY0Lmx0GmshaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAEp8BCgNsdGUYAyABKBBCjwHCSIsBCogBCgxzZml4ZWQ2NC5sdGUaeCFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID4gcnVsZXMubHRlPyAndmFsdWUgbXVzdCBiZSBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMubHRlXSkgOiAnJ0gAEqYHCgJndBgEIAEoEEKXB8JIkwcKfQoLc2ZpeGVkNjQuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrYBCg5zZml4ZWQ2NC5ndF9sdBqjAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKvgEKGHNmaXhlZDY0Lmd0X2x0X2V4Y2x1c2l2ZRqhAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndCAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCsYBCg9zZml4ZWQ2NC5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCs4BChlzZml4ZWQ2NC5ndF9sdGVfZXhjbHVzaXZlGrABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARLyBwoDZ3RlGAUgASgQQuIHwkjeBwqLAQoMc2ZpeGVkNjQuZ3RlGnshaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8IHJ1bGVzLmd0ZT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZV0pIDogJycKxQEKD3NmaXhlZDY0Lmd0ZV9sdBqxAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrNAQoZc2ZpeGVkNjQuZ3RlX2x0X2V4Y2x1c2l2ZRqvAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycK1QEKEHNmaXhlZDY0Lmd0ZV9sdGUawAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3RlICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJycK3QEKGnNmaXhlZDY0Lmd0ZV9sdGVfZXhjbHVzaXZlGr4BaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEoIBCgJpbhgGIAMoEEJ2wkhzCnEKC3NmaXhlZDY0LmluGmIhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICd2YWx1ZSBtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJ5CgZub3RfaW4YByADKBBCacJIZgpkCg9zZml4ZWQ2NC5ub3RfaW4aUXRoaXMgaW4gcnVsZXMubm90X2luID8gJ3ZhbHVlIG11c3Qgbm90IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbcnVsZXMubm90X2luXSkgOiAnJxIuCgdleGFtcGxlGAggAygQQh3CSBoKGAoQc2ZpeGVkNjQuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4ixwEKCUJvb2xSdWxlcxKCAQoFY29uc3QYASABKAhCc8JIcApuCgpib29sLmNvbnN0GmB0aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICd2YWx1ZSBtdXN0IGVxdWFsICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSKgoHZXhhbXBsZRgCIAMoCEIZwkgWChQKDGJvb2wuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACIpA3CgtTdHJpbmdSdWxlcxKGAQoFY29uc3QYASABKAlCd8JIdApyCgxzdHJpbmcuY29uc3QaYnRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgYCVzYCcuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEn4KA2xlbhgTIAEoBEJxwkhuCmwKCnN0cmluZy5sZW4aXnVpbnQodGhpcy5zaXplKCkpICE9IHJ1bGVzLmxlbiA/ICd2YWx1ZSBsZW5ndGggbXVzdCBiZSAlcyBjaGFyYWN0ZXJzJy5mb3JtYXQoW3J1bGVzLmxlbl0pIDogJycSmQEKB21pbl9sZW4YAiABKARChwHCSIMBCoABCg5zdHJpbmcubWluX2xlbhpudWludCh0aGlzLnNpemUoKSkgPCBydWxlcy5taW5fbGVuID8gJ3ZhbHVlIGxlbmd0aCBtdXN0IGJlIGF0IGxlYXN0ICVzIGNoYXJhY3RlcnMnLmZvcm1hdChbcnVsZXMubWluX2xlbl0pIDogJycSlwEKB21heF9sZW4YAyABKARChQHCSIEBCn8KDnN0cmluZy5tYXhfbGVuGm11aW50KHRoaXMuc2l6ZSgpKSA+IHJ1bGVzLm1heF9sZW4gPyAndmFsdWUgbGVuZ3RoIG11c3QgYmUgYXQgbW9zdCAlcyBjaGFyYWN0ZXJzJy5mb3JtYXQoW3J1bGVzLm1heF9sZW5dKSA6ICcnEpsBCglsZW5fYnl0ZXMYFCABKARChwHCSIMBCoABChBzdHJpbmcubGVuX2J5dGVzGmx1aW50KGJ5dGVzKHRoaXMpLnNpemUoKSkgIT0gcnVsZXMubGVuX2J5dGVzID8gJ3ZhbHVlIGxlbmd0aCBtdXN0IGJlICVzIGJ5dGVzJy5mb3JtYXQoW3J1bGVzLmxlbl9ieXRlc10pIDogJycSowEKCW1pbl9ieXRlcxgEIAEoBEKPAcJIiwEKiAEKEHN0cmluZy5taW5fYnl0ZXMadHVpbnQoYnl0ZXModGhpcykuc2l6ZSgpKSA8IHJ1bGVzLm1pbl9ieXRlcyA/ICd2YWx1ZSBsZW5ndGggbXVzdCBiZSBhdCBsZWFzdCAlcyBieXRlcycuZm9ybWF0KFtydWxlcy5taW5fYnl0ZXNdKSA6ICcnEqIBCgltYXhfYnl0ZXMYBSABKARCjgHCSIoBCocBChBzdHJpbmcubWF4X2J5dGVzGnN1aW50KGJ5dGVzKHRoaXMpLnNpemUoKSkgPiBydWxlcy5tYXhfYnl0ZXMgPyAndmFsdWUgbGVuZ3RoIG11c3QgYmUgYXQgbW9zdCAlcyBieXRlcycuZm9ybWF0KFtydWxlcy5tYXhfYnl0ZXNdKSA6ICcnEo0BCgdwYXR0ZXJuGAYgASgJQnzCSHkKdwoOc3RyaW5nLnBhdHRlcm4aZSF0aGlzLm1hdGNoZXMocnVsZXMucGF0dGVybikgPyAndmFsdWUgZG9lcyBub3QgbWF0Y2ggcmVnZXggcGF0dGVybiBgJXNgJy5mb3JtYXQoW3J1bGVzLnBhdHRlcm5dKSA6ICcnEoQBCgZwcmVmaXgYByABKAlCdMJIcQpvCg1zdHJpbmcucHJlZml4Gl4hdGhpcy5zdGFydHNXaXRoKHJ1bGVzLnByZWZpeCkgPyAndmFsdWUgZG9lcyBub3QgaGF2ZSBwcmVmaXggYCVzYCcuZm9ybWF0KFtydWxlcy5wcmVmaXhdKSA6ICcnEoIBCgZzdWZmaXgYCCABKAlCcsJIbwptCg1zdHJpbmcuc3VmZml4GlwhdGhpcy5lbmRzV2l0aChydWxlcy5zdWZmaXgpID8gJ3ZhbHVlIGRvZXMgbm90IGhhdmUgc3VmZml4IGAlc2AnLmZvcm1hdChbcnVsZXMuc3VmZml4XSkgOiAnJxKQAQoIY29udGFpbnMYCSABKAlCfsJIewp5Cg9zdHJpbmcuY29udGFpbnMaZiF0aGlzLmNvbnRhaW5zKHJ1bGVzLmNvbnRhaW5zKSA/ICd2YWx1ZSBkb2VzIG5vdCBjb250YWluIHN1YnN0cmluZyBgJXNgJy5mb3JtYXQoW3J1bGVzLmNvbnRhaW5zXSkgOiAnJxKYAQoMbm90X2NvbnRhaW5zGBcgASgJQoEBwkh+CnwKE3N0cmluZy5ub3RfY29udGFpbnMaZXRoaXMuY29udGFpbnMocnVsZXMubm90X2NvbnRhaW5zKSA/ICd2YWx1ZSBjb250YWlucyBzdWJzdHJpbmcgYCVzYCcuZm9ybWF0KFtydWxlcy5ub3RfY29udGFpbnNdKSA6ICcnEoABCgJpbhgKIAMoCUJ0wkhxCm8KCXN0cmluZy5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSdwoGbm90X2luGAsgAygJQmfCSGQKYgoNc3RyaW5nLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEt8BCgVlbWFpbBgMIAEoCELNAcJIyQEKYQoMc3RyaW5nLmVtYWlsEiN2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgZW1haWwgYWRkcmVzcxosIXJ1bGVzLmVtYWlsIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5pc0VtYWlsKCkKZAoSc3RyaW5nLmVtYWlsX2VtcHR5EjJ2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgZW1haWwgYWRkcmVzcxoaIXJ1bGVzLmVtYWlsIHx8IHRoaXMgIT0gJydIABLnAQoIaG9zdG5hbWUYDSABKAhC0gHCSM4BCmUKD3N0cmluZy5ob3N0bmFtZRIedmFsdWUgbXVzdCBiZSBhIHZhbGlkIGhvc3RuYW1lGjIhcnVsZXMuaG9zdG5hbWUgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSG9zdG5hbWUoKQplChVzdHJpbmcuaG9zdG5hbWVfZW1wdHkSLXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBob3N0bmFtZRodIXJ1bGVzLmhvc3RuYW1lIHx8IHRoaXMgIT0gJydIABLHAQoCaXAYDiABKAhCuAHCSLQBClUKCXN0cmluZy5pcBIgdmFsdWUgbXVzdCBiZSBhIHZhbGlkIElQIGFkZHJlc3MaJiFydWxlcy5pcCB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNJcCgpClsKD3N0cmluZy5pcF9lbXB0eRIvdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQIGFkZHJlc3MaFyFydWxlcy5pcCB8fCB0aGlzICE9ICcnSAAS1gEKBGlwdjQYDyABKAhCxQHCSMEBClwKC3N0cmluZy5pcHY0EiJ2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgSVB2NCBhZGRyZXNzGikhcnVsZXMuaXB2NCB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNJcCg0KQphChFzdHJpbmcuaXB2NF9lbXB0eRIxdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQdjQgYWRkcmVzcxoZIXJ1bGVzLmlwdjQgfHwgdGhpcyAhPSAnJ0gAEtYBCgRpcHY2GBAgASgIQsUBwkjBAQpcCgtzdHJpbmcuaXB2NhIidmFsdWUgbXVzdCBiZSBhIHZhbGlkIElQdjYgYWRkcmVzcxopIXJ1bGVzLmlwdjYgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSXAoNikKYQoRc3RyaW5nLmlwdjZfZW1wdHkSMXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBJUHY2IGFkZHJlc3MaGSFydWxlcy5pcHY2IHx8IHRoaXMgIT0gJydIABK/AQoDdXJpGBEgASgIQq8BwkirAQpRCgpzdHJpbmcudXJpEhl2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgVVJJGighcnVsZXMudXJpIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5pc1VyaSgpClYKEHN0cmluZy51cmlfZW1wdHkSKHZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBVUkkaGCFydWxlcy51cmkgfHwgdGhpcyAhPSAnJ0gAEnAKB3VyaV9yZWYYEiABKAhCXcJIWgpYCg5zdHJpbmcudXJpX3JlZhIjdmFsdWUgbXVzdCBiZSBhIHZhbGlkIFVSSSBSZWZlcmVuY2UaISFydWxlcy51cmlfcmVmIHx8IHRoaXMuaXNVcmlSZWYoKUgAEpACCgdhZGRyZXNzGBUgASgIQvwBwkj4AQqBAQoOc3RyaW5nLmFkZHJlc3MSLXZhbHVlIG11c3QgYmUgYSB2YWxpZCBob3N0bmFtZSwgb3IgaXAgYWRkcmVzcxpAIXJ1bGVzLmFkZHJlc3MgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSG9zdG5hbWUoKSB8fCB0aGlzLmlzSXAoKQpyChRzdHJpbmcuYWRkcmVzc19lbXB0eRI8dmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIGhvc3RuYW1lLCBvciBpcCBhZGRyZXNzGhwhcnVsZXMuYWRkcmVzcyB8fCB0aGlzICE9ICcnSAASmAIKBHV1aWQYFiABKAhChwLCSIMCCqUBCgtzdHJpbmcudXVpZBIadmFsdWUgbXVzdCBiZSBhIHZhbGlkIFVVSUQaeiFydWxlcy51dWlkIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5tYXRjaGVzKCdeWzAtOWEtZkEtRl17OH0tWzAtOWEtZkEtRl17NH0tWzAtOWEtZkEtRl17NH0tWzAtOWEtZkEtRl17NH0tWzAtOWEtZkEtRl17MTJ9JCcpClkKEXN0cmluZy51dWlkX2VtcHR5Eil2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgVVVJRBoZIXJ1bGVzLnV1aWQgfHwgdGhpcyAhPSAnJ0gAEvABCgV0dXVpZBghIAEoCELeAcJI2gEKcwoMc3RyaW5nLnR1dWlkEiJ2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgdHJpbW1lZCBVVUlEGj8hcnVsZXMudHV1aWQgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLm1hdGNoZXMoJ15bMC05YS1mQS1GXXszMn0kJykKYwoSc3RyaW5nLnR1dWlkX2VtcHR5EjF2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgdHJpbW1lZCBVVUlEGhohcnVsZXMudHV1aWQgfHwgdGhpcyAhPSAnJ0gAEpYCChFpcF93aXRoX3ByZWZpeGxlbhgaIAEoCEL4AcJI9AEKeAoYc3RyaW5nLmlwX3dpdGhfcHJlZml4bGVuEh92YWx1ZSBtdXN0IGJlIGEgdmFsaWQgSVAgcHJlZml4GjshcnVsZXMuaXBfd2l0aF9wcmVmaXhsZW4gfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSXBQcmVmaXgoKQp4Ch5zdHJpbmcuaXBfd2l0aF9wcmVmaXhsZW5fZW1wdHkSLnZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBJUCBwcmVmaXgaJiFydWxlcy5pcF93aXRoX3ByZWZpeGxlbiB8fCB0aGlzICE9ICcnSAASzwIKE2lwdjRfd2l0aF9wcmVmaXhsZW4YGyABKAhCrwLCSKsCCpMBChpzdHJpbmcuaXB2NF93aXRoX3ByZWZpeGxlbhI1dmFsdWUgbXVzdCBiZSBhIHZhbGlkIElQdjQgYWRkcmVzcyB3aXRoIHByZWZpeCBsZW5ndGgaPiFydWxlcy5pcHY0X3dpdGhfcHJlZml4bGVuIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5pc0lwUHJlZml4KDQpCpIBCiBzdHJpbmcuaXB2NF93aXRoX3ByZWZpeGxlbl9lbXB0eRJEdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQdjQgYWRkcmVzcyB3aXRoIHByZWZpeCBsZW5ndGgaKCFydWxlcy5pcHY0X3dpdGhfcHJlZml4bGVuIHx8IHRoaXMgIT0gJydIABLPAgoTaXB2Nl93aXRoX3ByZWZpeGxlbhgcIAEoCEKvAsJIqwIKkwEKGnN0cmluZy5pcHY2X3dpdGhfcHJlZml4bGVuEjV2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgSVB2NiBhZGRyZXNzIHdpdGggcHJlZml4IGxlbmd0aBo+IXJ1bGVzLmlwdjZfd2l0aF9wcmVmaXhsZW4gfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSXBQcmVmaXgoNikKkgEKIHN0cmluZy5pcHY2X3dpdGhfcHJlZml4bGVuX2VtcHR5EkR2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgSVB2NiBhZGRyZXNzIHdpdGggcHJlZml4IGxlbmd0aBooIXJ1bGVzLmlwdjZfd2l0aF9wcmVmaXhsZW4gfHwgdGhpcyAhPSAnJ0gAEvIBCglpcF9wcmVmaXgYHSABKAhC3AHCSNgBCmwKEHN0cmluZy5pcF9wcmVmaXgSH3ZhbHVlIG11c3QgYmUgYSB2YWxpZCBJUCBwcmVmaXgaNyFydWxlcy5pcF9wcmVmaXggfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSXBQcmVmaXgodHJ1ZSkKaAoWc3RyaW5nLmlwX3ByZWZpeF9lbXB0eRIudmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQIHByZWZpeBoeIXJ1bGVzLmlwX3ByZWZpeCB8fCB0aGlzICE9ICcnSAASgwIKC2lwdjRfcHJlZml4GB4gASgIQusBwkjnAQp1ChJzdHJpbmcuaXB2NF9wcmVmaXgSIXZhbHVlIG11c3QgYmUgYSB2YWxpZCBJUHY0IHByZWZpeBo8IXJ1bGVzLmlwdjRfcHJlZml4IHx8IHRoaXMgPT0gJycgfHwgdGhpcy5pc0lwUHJlZml4KDQsIHRydWUpCm4KGHN0cmluZy5pcHY0X3ByZWZpeF9lbXB0eRIwdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQdjQgcHJlZml4GiAhcnVsZXMuaXB2NF9wcmVmaXggfHwgdGhpcyAhPSAnJ0gAEoMCCgtpcHY2X3ByZWZpeBgfIAEoCELrAcJI5wEKdQoSc3RyaW5nLmlwdjZfcHJlZml4EiF2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgSVB2NiBwcmVmaXgaPCFydWxlcy5pcHY2X3ByZWZpeCB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNJcFByZWZpeCg2LCB0cnVlKQpuChhzdHJpbmcuaXB2Nl9wcmVmaXhfZW1wdHkSMHZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBJUHY2IHByZWZpeBogIXJ1bGVzLmlwdjZfcHJlZml4IHx8IHRoaXMgIT0gJydIABK1AgoNaG9zdF9hbmRfcG9ydBggIAEoCEKbAsJIlwIKmQEKFHN0cmluZy5ob3N0X2FuZF9wb3J0EkF2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgaG9zdCAoaG9zdG5hbWUgb3IgSVAgYWRkcmVzcykgYW5kIHBvcnQgcGFpcho+IXJ1bGVzLmhvc3RfYW5kX3BvcnQgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSG9zdEFuZFBvcnQodHJ1ZSkKeQoac3RyaW5nLmhvc3RfYW5kX3BvcnRfZW1wdHkSN3ZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBob3N0IGFuZCBwb3J0IHBhaXIaIiFydWxlcy5ob3N0X2FuZF9wb3J0IHx8IHRoaXMgIT0gJydIABKoBQoQd2VsbF9rbm93bl9yZWdleBgYIAEoDjIYLmJ1Zi52YWxpZGF0ZS5Lbm93blJlZ2V4QvEEwkjtBArwAQojc3RyaW5nLndlbGxfa25vd25fcmVnZXguaGVhZGVyX25hbWUSJnZhbHVlIG11c3QgYmUgYSB2YWxpZCBIVFRQIGhlYWRlciBuYW1lGqABcnVsZXMud2VsbF9rbm93bl9yZWdleCAhPSAxIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5tYXRjaGVzKCFoYXMocnVsZXMuc3RyaWN0KSB8fCBydWxlcy5zdHJpY3QgPydeOj9bMC05YS16QS1aISMkJSZcJyorLS5eX3x+XHg2MF0rJCcgOideW15cdTAwMDBcdTAwMEFcdTAwMERdKyQnKQqNAQopc3RyaW5nLndlbGxfa25vd25fcmVnZXguaGVhZGVyX25hbWVfZW1wdHkSNXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBIVFRQIGhlYWRlciBuYW1lGilydWxlcy53ZWxsX2tub3duX3JlZ2V4ICE9IDEgfHwgdGhpcyAhPSAnJwrnAQokc3RyaW5nLndlbGxfa25vd25fcmVnZXguaGVhZGVyX3ZhbHVlEid2YWx1ZSBtdXN0IGJlIGEgdmFsaWQgSFRUUCBoZWFkZXIgdmFsdWUalQFydWxlcy53ZWxsX2tub3duX3JlZ2V4ICE9IDIgfHwgdGhpcy5tYXRjaGVzKCFoYXMocnVsZXMuc3RyaWN0KSB8fCBydWxlcy5zdHJpY3QgPydeW15cdTAwMDAtXHUwMDA4XHUwMDBBLVx1MDAxRlx1MDA3Rl0qJCcgOideW15cdTAwMDBcdTAwMEFcdTAwMERdKiQnKUgAEg4KBnN0cmljdBgZIAEoCBIsCgdleGFtcGxlGCIgAygJQhvCSBgKFgoOc3RyaW5nLmV4YW1wbGUaBHRydWUqCQjoBxCAgICAAkIMCgp3ZWxsX2tub3duIuoQCgpCeXRlc1J1bGVzEoABCgVjb25zdBgBIAEoDEJxwkhuCmwKC2J5dGVzLmNvbnN0Gl10aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICd2YWx1ZSBtdXN0IGJlICV4Jy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSeAoDbGVuGA0gASgEQmvCSGgKZgoJYnl0ZXMubGVuGll1aW50KHRoaXMuc2l6ZSgpKSAhPSBydWxlcy5sZW4gPyAndmFsdWUgbGVuZ3RoIG11c3QgYmUgJXMgYnl0ZXMnLmZvcm1hdChbcnVsZXMubGVuXSkgOiAnJxKQAQoHbWluX2xlbhgCIAEoBEJ/wkh8CnoKDWJ5dGVzLm1pbl9sZW4aaXVpbnQodGhpcy5zaXplKCkpIDwgcnVsZXMubWluX2xlbiA/ICd2YWx1ZSBsZW5ndGggbXVzdCBiZSBhdCBsZWFzdCAlcyBieXRlcycuZm9ybWF0KFtydWxlcy5taW5fbGVuXSkgOiAnJxKIAQoHbWF4X2xlbhgDIAEoBEJ3wkh0CnIKDWJ5dGVzLm1heF9sZW4aYXVpbnQodGhpcy5zaXplKCkpID4gcnVsZXMubWF4X2xlbiA/ICd2YWx1ZSBtdXN0IGJlIGF0IG1vc3QgJXMgYnl0ZXMnLmZvcm1hdChbcnVsZXMubWF4X2xlbl0pIDogJycSkAEKB3BhdHRlcm4YBCABKAlCf8JIfAp6Cg1ieXRlcy5wYXR0ZXJuGmkhc3RyaW5nKHRoaXMpLm1hdGNoZXMocnVsZXMucGF0dGVybikgPyAndmFsdWUgbXVzdCBtYXRjaCByZWdleCBwYXR0ZXJuIGAlc2AnLmZvcm1hdChbcnVsZXMucGF0dGVybl0pIDogJycSgQEKBnByZWZpeBgFIAEoDEJxwkhuCmwKDGJ5dGVzLnByZWZpeBpcIXRoaXMuc3RhcnRzV2l0aChydWxlcy5wcmVmaXgpID8gJ3ZhbHVlIGRvZXMgbm90IGhhdmUgcHJlZml4ICV4Jy5mb3JtYXQoW3J1bGVzLnByZWZpeF0pIDogJycSfwoGc3VmZml4GAYgASgMQm/CSGwKagoMYnl0ZXMuc3VmZml4GlohdGhpcy5lbmRzV2l0aChydWxlcy5zdWZmaXgpID8gJ3ZhbHVlIGRvZXMgbm90IGhhdmUgc3VmZml4ICV4Jy5mb3JtYXQoW3J1bGVzLnN1ZmZpeF0pIDogJycSgwEKCGNvbnRhaW5zGAcgASgMQnHCSG4KbAoOYnl0ZXMuY29udGFpbnMaWiF0aGlzLmNvbnRhaW5zKHJ1bGVzLmNvbnRhaW5zKSA/ICd2YWx1ZSBkb2VzIG5vdCBjb250YWluICV4Jy5mb3JtYXQoW3J1bGVzLmNvbnRhaW5zXSkgOiAnJxKnAQoCaW4YCCADKAxCmgHCSJYBCpMBCghieXRlcy5pbhqGAWdldEZpZWxkKHJ1bGVzLCAnaW4nKS5zaXplKCkgPiAwICYmICEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ3ZhbHVlIG11c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEnYKBm5vdF9pbhgJIAMoDEJmwkhjCmEKDGJ5dGVzLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEusBCgJpcBgKIAEoCELcAcJI2AEKdAoIYnl0ZXMuaXASIHZhbHVlIG11c3QgYmUgYSB2YWxpZCBJUCBhZGRyZXNzGkYhcnVsZXMuaXAgfHwgdGhpcy5zaXplKCkgPT0gMCB8fCB0aGlzLnNpemUoKSA9PSA0IHx8IHRoaXMuc2l6ZSgpID09IDE2CmAKDmJ5dGVzLmlwX2VtcHR5Ei92YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgSVAgYWRkcmVzcxodIXJ1bGVzLmlwIHx8IHRoaXMuc2l6ZSgpICE9IDBIABLkAQoEaXB2NBgLIAEoCELTAcJIzwEKZQoKYnl0ZXMuaXB2NBIidmFsdWUgbXVzdCBiZSBhIHZhbGlkIElQdjQgYWRkcmVzcxozIXJ1bGVzLmlwdjQgfHwgdGhpcy5zaXplKCkgPT0gMCB8fCB0aGlzLnNpemUoKSA9PSA0CmYKEGJ5dGVzLmlwdjRfZW1wdHkSMXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBJUHY0IGFkZHJlc3MaHyFydWxlcy5pcHY0IHx8IHRoaXMuc2l6ZSgpICE9IDBIABLlAQoEaXB2NhgMIAEoCELUAcJI0AEKZgoKYnl0ZXMuaXB2NhIidmFsdWUgbXVzdCBiZSBhIHZhbGlkIElQdjYgYWRkcmVzcxo0IXJ1bGVzLmlwdjYgfHwgdGhpcy5zaXplKCkgPT0gMCB8fCB0aGlzLnNpemUoKSA9PSAxNgpmChBieXRlcy5pcHY2X2VtcHR5EjF2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgSVB2NiBhZGRyZXNzGh8hcnVsZXMuaXB2NiB8fCB0aGlzLnNpemUoKSAhPSAwSAASKwoHZXhhbXBsZRgOIAMoDEIawkgXChUKDWJ5dGVzLmV4YW1wbGUaBHRydWUqCQjoBxCAgICAAkIMCgp3ZWxsX2tub3duItQDCglFbnVtUnVsZXMSggEKBWNvbnN0GAEgASgFQnPCSHAKbgoKZW51bS5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEhQKDGRlZmluZWRfb25seRgCIAEoCBJ+CgJpbhgDIAMoBUJywkhvCm0KB2VudW0uaW4aYiEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ3ZhbHVlIG11c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEnUKBm5vdF9pbhgEIAMoBUJlwkhiCmAKC2VudW0ubm90X2luGlF0aGlzIGluIHJ1bGVzLm5vdF9pbiA/ICd2YWx1ZSBtdXN0IG5vdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW3J1bGVzLm5vdF9pbl0pIDogJycSKgoHZXhhbXBsZRgFIAMoBUIZwkgWChQKDGVudW0uZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACIvsDCg1SZXBlYXRlZFJ1bGVzEp4BCgltaW5faXRlbXMYASABKARCigHCSIYBCoMBChJyZXBlYXRlZC5taW5faXRlbXMabXVpbnQodGhpcy5zaXplKCkpIDwgcnVsZXMubWluX2l0ZW1zID8gJ3ZhbHVlIG11c3QgY29udGFpbiBhdCBsZWFzdCAlZCBpdGVtKHMpJy5mb3JtYXQoW3J1bGVzLm1pbl9pdGVtc10pIDogJycSogEKCW1heF9pdGVtcxgCIAEoBEKOAcJIigEKhwEKEnJlcGVhdGVkLm1heF9pdGVtcxpxdWludCh0aGlzLnNpemUoKSkgPiBydWxlcy5tYXhfaXRlbXMgPyAndmFsdWUgbXVzdCBjb250YWluIG5vIG1vcmUgdGhhbiAlcyBpdGVtKHMpJy5mb3JtYXQoW3J1bGVzLm1heF9pdGVtc10pIDogJycScAoGdW5pcXVlGAMgASgIQmDCSF0KWwoPcmVwZWF0ZWQudW5pcXVlEihyZXBlYXRlZCB2YWx1ZSBtdXN0IGNvbnRhaW4gdW5pcXVlIGl0ZW1zGh4hcnVsZXMudW5pcXVlIHx8IHRoaXMudW5pcXVlKCkSJwoFaXRlbXMYBCABKAsyGC5idWYudmFsaWRhdGUuRmllbGRSdWxlcyoJCOgHEICAgIACIooDCghNYXBSdWxlcxKPAQoJbWluX3BhaXJzGAEgASgEQnzCSHkKdwoNbWFwLm1pbl9wYWlycxpmdWludCh0aGlzLnNpemUoKSkgPCBydWxlcy5taW5fcGFpcnMgPyAnbWFwIG11c3QgYmUgYXQgbGVhc3QgJWQgZW50cmllcycuZm9ybWF0KFtydWxlcy5taW5fcGFpcnNdKSA6ICcnEo4BCgltYXhfcGFpcnMYAiABKARCe8JIeAp2Cg1tYXAubWF4X3BhaXJzGmV1aW50KHRoaXMuc2l6ZSgpKSA+IHJ1bGVzLm1heF9wYWlycyA/ICdtYXAgbXVzdCBiZSBhdCBtb3N0ICVkIGVudHJpZXMnLmZvcm1hdChbcnVsZXMubWF4X3BhaXJzXSkgOiAnJxImCgRrZXlzGAQgASgLMhguYnVmLnZhbGlkYXRlLkZpZWxkUnVsZXMSKAoGdmFsdWVzGAUgASgLMhguYnVmLnZhbGlkYXRlLkZpZWxkUnVsZXMqCQjoBxCAgICAAiImCghBbnlSdWxlcxIKCgJpbhgCIAMoCRIOCgZub3RfaW4YAyADKAkimRcKDUR1cmF0aW9uUnVsZXMSoQEKBWNvbnN0GAIgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uQnfCSHQKcgoOZHVyYXRpb24uY29uc3QaYHRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ3ZhbHVlIG11c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKoAQoCbHQYAyABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25Cf8JIfAp6CgtkdXJhdGlvbi5sdBprIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABK6AQoDbHRlGAQgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uQo8BwkiLAQqIAQoMZHVyYXRpb24ubHRlGnghaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+IHJ1bGVzLmx0ZT8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABLBBwoCZ3QYBSABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25ClwfCSJMHCn0KC2R1cmF0aW9uLmd0Gm4haGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwq2AQoOZHVyYXRpb24uZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCr4BChhkdXJhdGlvbi5ndF9sdF9leGNsdXNpdmUaoQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrGAQoPZHVyYXRpb24uZ3RfbHRlGrIBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrOAQoZZHVyYXRpb24uZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAESjQgKA2d0ZRgGIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkLiB8JI3gcKiwEKDGR1cmF0aW9uLmd0ZRp7IWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPCBydWxlcy5ndGU/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCsUBCg9kdXJhdGlvbi5ndGVfbHQasQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKzQEKGWR1cmF0aW9uLmd0ZV9sdF9leGNsdXNpdmUarwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtUBChBkdXJhdGlvbi5ndGVfbHRlGsABaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnCt0BChpkdXJhdGlvbi5ndGVfbHRlX2V4Y2x1c2l2ZRq+AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARKdAQoCaW4YByADKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CdsJIcwpxCgtkdXJhdGlvbi5pbhpiISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAndmFsdWUgbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycSlAEKBm5vdF9pbhgIIAMoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkJpwkhmCmQKD2R1cmF0aW9uLm5vdF9pbhpRdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAndmFsdWUgbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEkkKB2V4YW1wbGUYCSADKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CHcJIGgoYChBkdXJhdGlvbi5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiKSGAoOVGltZXN0YW1wUnVsZXMSowEKBWNvbnN0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEJ4wkh1CnMKD3RpbWVzdGFtcC5jb25zdBpgdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAndmFsdWUgbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEqsBCgJsdBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCgAHCSH0KewoMdGltZXN0YW1wLmx0GmshaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ3ZhbHVlIG11c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAErwBCgNsdGUYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQpABwkiMAQqJAQoNdGltZXN0YW1wLmx0ZRp4IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASbAoGbHRfbm93GAcgASgIQlrCSFcKVQoQdGltZXN0YW1wLmx0X25vdxpBKHJ1bGVzLmx0X25vdyAmJiB0aGlzID4gbm93KSA/ICd2YWx1ZSBtdXN0IGJlIGxlc3MgdGhhbiBub3cnIDogJydIABLHBwoCZ3QYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQpwHwkiYBwp+Cgx0aW1lc3RhbXAuZ3QabiFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrcBCg90aW1lc3RhbXAuZ3RfbHQaowFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCr8BChl0aW1lc3RhbXAuZ3RfbHRfZXhjbHVzaXZlGqEBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKxwEKEHRpbWVzdGFtcC5ndF9sdGUasgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICd2YWx1ZSBtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCs8BChp0aW1lc3RhbXAuZ3RfbHRlX2V4Y2x1c2l2ZRqwAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAESkwgKA2d0ZRgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBC5wfCSOMHCowBCg10aW1lc3RhbXAuZ3RlGnshaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8IHJ1bGVzLmd0ZT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZV0pIDogJycKxgEKEHRpbWVzdGFtcC5ndGVfbHQasQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKzgEKGnRpbWVzdGFtcC5ndGVfbHRfZXhjbHVzaXZlGq8BaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrWAQoRdGltZXN0YW1wLmd0ZV9sdGUawAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3RlICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAndmFsdWUgbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJycK3gEKG3RpbWVzdGFtcC5ndGVfbHRlX2V4Y2x1c2l2ZRq+AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARJvCgZndF9ub3cYCCABKAhCXcJIWgpYChB0aW1lc3RhbXAuZ3Rfbm93GkQocnVsZXMuZ3Rfbm93ICYmIHRoaXMgPCBub3cpID8gJ3ZhbHVlIG11c3QgYmUgZ3JlYXRlciB0aGFuIG5vdycgOiAnJ0gBErgBCgZ3aXRoaW4YCSABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CjAHCSIgBCoUBChB0aW1lc3RhbXAud2l0aGluGnF0aGlzIDwgbm93LXJ1bGVzLndpdGhpbiB8fCB0aGlzID4gbm93K3J1bGVzLndpdGhpbiA/ICd2YWx1ZSBtdXN0IGJlIHdpdGhpbiAlcyBvZiBub3cnLmZvcm1hdChbcnVsZXMud2l0aGluXSkgOiAnJxJLCgdleGFtcGxlGAogAygLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEIewkgbChkKEXRpbWVzdGFtcC5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiI5CgpWaW9sYXRpb25zEisKCnZpb2xhdGlvbnMYASADKAsyFy5idWYudmFsaWRhdGUuVmlvbGF0aW9uIp8BCglWaW9sYXRpb24SJgoFZmllbGQYBSABKAsyFy5idWYudmFsaWRhdGUuRmllbGRQYXRoEiUKBHJ1bGUYBiABKAsyFy5idWYudmFsaWRhdGUuRmllbGRQYXRoEg8KB3J1bGVfaWQYAiABKAkSDwoHbWVzc2FnZRgDIAEoCRIPCgdmb3Jfa2V5GAQgASgISgQIARACUgpmaWVsZF9wYXRoIj0KCUZpZWxkUGF0aBIwCghlbGVtZW50cxgBIAMoCzIeLmJ1Zi52YWxpZGF0ZS5GaWVsZFBhdGhFbGVtZW50IukCChBGaWVsZFBhdGhFbGVtZW50EhQKDGZpZWxkX251bWJlchgBIAEoBRISCgpmaWVsZF9uYW1lGAIgASgJEj4KCmZpZWxkX3R5cGUYAyABKA4yKi5nb29nbGUucHJvdG9idWYuRmllbGREZXNjcmlwdG9yUHJvdG8uVHlwZRI8CghrZXlfdHlwZRgEIAEoDjIqLmdvb2dsZS5wcm90b2J1Zi5GaWVsZERlc2NyaXB0b3JQcm90by5UeXBlEj4KCnZhbHVlX3R5cGUYBSABKA4yKi5nb29nbGUucHJvdG9idWYuRmllbGREZXNjcmlwdG9yUHJvdG8uVHlwZRIPCgVpbmRleBgGIAEoBEgAEhIKCGJvb2xfa2V5GAcgASgISAASEQoHaW50X2tleRgIIAEoA0gAEhIKCHVpbnRfa2V5GAkgASgESAASFAoKc3RyaW5nX2tleRgKIAEoCUgAQgsKCXN1YnNjcmlwdCqhAQoGSWdub3JlEhYKEklHTk9SRV9VTlNQRUNJRklFRBAAEhgKFElHTk9SRV9JRl9aRVJPX1ZBTFVFEAESEQoNSUdOT1JFX0FMV0FZUxADIgQIAhACKgxJR05PUkVfRU1QVFkqDklHTk9SRV9ERUZBVUxUKhdJR05PUkVfSUZfREVGQVVMVF9WQUxVRSoVSUdOT1JFX0lGX1VOUE9QVUxBVEVEKm4KCktub3duUmVnZXgSGwoXS05PV05fUkVHRVhfVU5TUEVDSUZJRUQQABIgChxLTk9XTl9SRUdFWF9IVFRQX0hFQURFUl9OQU1FEAESIQodS05PV05fUkVHRVhfSFRUUF9IRUFERVJfVkFMVUUQAjpWCgdtZXNzYWdlEh8uZ29vZ2xlLnByb3RvYnVmLk1lc3NhZ2VPcHRpb25zGIcJIAEoCzIaLmJ1Zi52YWxpZGF0ZS5NZXNzYWdlUnVsZXNSB21lc3NhZ2U6TgoFb25lb2YSHS5nb29nbGUucHJvdG9idWYuT25lb2ZPcHRpb25zGIcJIAEoCzIYLmJ1Zi52YWxpZGF0ZS5PbmVvZlJ1bGVzUgVvbmVvZjpOCgVmaWVsZBIdLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE9wdGlvbnMYhwkgASgLMhguYnVmLnZhbGlkYXRlLkZpZWxkUnVsZXNSBWZpZWxkOl0KCnByZWRlZmluZWQSHS5nb29nbGUucHJvdG9idWYuRmllbGRPcHRpb25zGIgJIAEoCzIdLmJ1Zi52YWxpZGF0ZS5QcmVkZWZpbmVkUnVsZXNSCnByZWRlZmluZWRCbgoSYnVpbGQuYnVmLnZhbGlkYXRlQg1WYWxpZGF0ZVByb3RvUAFaR2J1Zi5idWlsZC9nZW4vZ28vYnVmYnVpbGQvcHJvdG92YWxpZGF0ZS9wcm90b2NvbGJ1ZmZlcnMvZ28vYnVmL3ZhbGlkYXRl\", [file_google_protobuf_descriptor, file_google_protobuf_duration, file_google_protobuf_timestamp]);\n\n/**\n * `Rule` represents a validation rule written in the Common Expression\n * Language (CEL) syntax. Each Rule includes a unique identifier, an\n * optional error message, and the CEL expression to evaluate. For more\n * information, [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).\n *\n * ```proto\n * message Foo {\n *   option (buf.validate.message).cel = {\n *     id: \"foo.bar\"\n *     message: \"bar must be greater than 0\"\n *     expression: \"this.bar > 0\"\n *   };\n *   int32 bar = 1;\n * }\n * ```\n *\n * @generated from message buf.validate.Rule\n */\nexport type Rule = Message<\"buf.validate.Rule\"> & {\n  /**\n   * `id` is a string that serves as a machine-readable name for this Rule.\n   * It should be unique within its scope, which could be either a message or a field.\n   *\n   * @generated from field: optional string id = 1;\n   */\n  id: string;\n\n  /**\n   * `message` is an optional field that provides a human-readable error message\n   * for this Rule when the CEL expression evaluates to false. If a\n   * non-empty message is provided, any strings resulting from the CEL\n   * expression evaluation are ignored.\n   *\n   * @generated from field: optional string message = 2;\n   */\n  message: string;\n\n  /**\n   * `expression` is the actual CEL expression that will be evaluated for\n   * validation. This string must resolve to either a boolean or a string\n   * value. If the expression evaluates to false or a non-empty string, the\n   * validation is considered failed, and the message is rejected.\n   *\n   * @generated from field: optional string expression = 3;\n   */\n  expression: string;\n};\n\n/**\n * Describes the message buf.validate.Rule.\n * Use `create(RuleSchema)` to create a new message.\n */\nexport const RuleSchema: GenMessage<Rule> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 0);\n\n/**\n * MessageRules represents validation rules that are applied to the entire message.\n * It includes disabling options and a list of Rule messages representing Common Expression Language (CEL) validation rules.\n *\n * @generated from message buf.validate.MessageRules\n */\nexport type MessageRules = Message<\"buf.validate.MessageRules\"> & {\n  /**\n   * `cel` is a repeated field of type Rule. Each Rule specifies a validation rule to be applied to this message.\n   * These rules are written in Common Expression Language (CEL) syntax. For more information,\n   * [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).\n   *\n   *\n   * ```proto\n   * message MyMessage {\n   *   // The field `foo` must be greater than 42.\n   *   option (buf.validate.message).cel = {\n   *     id: \"my_message.value\",\n   *     message: \"value must be greater than 42\",\n   *     expression: \"this.foo > 42\",\n   *   };\n   *   optional int32 foo = 1;\n   * }\n   * ```\n   *\n   * @generated from field: repeated buf.validate.Rule cel = 3;\n   */\n  cel: Rule[];\n\n  /**\n   * `oneof` is a repeated field of type MessageOneofRule that specifies a list of fields\n   * of which at most one can be present. If `required` is also specified, then exactly one\n   * of the specified fields _must_ be present.\n   *\n   * This will enforce oneof-like constraints with a few features not provided by\n   * actual Protobuf oneof declarations:\n   *   1. Repeated and map fields are allowed in this validation. In a Protobuf oneof,\n   *      only scalar fields are allowed.\n   *   2. Fields with implicit presence are allowed. In a Protobuf oneof, all member\n   *      fields have explicit presence. This means that, for the purpose of determining\n   *      how many fields are set, explicitly setting such a field to its zero value is\n   *      effectively the same as not setting it at all.\n   *   3. This will always generate validation errors for a message unmarshalled from\n   *      serialized data that sets more than one field. With a Protobuf oneof, when\n   *      multiple fields are present in the serialized form, earlier values are usually\n   *      silently ignored when unmarshalling, with only the last field being set when\n   *      unmarshalling completes.\n   *\n   * Note that adding a field to a `oneof` will also set the IGNORE_IF_ZERO_VALUE on the fields. This means\n   * only the field that is set will be validated and the unset fields are not validated according to the field rules.\n   * This behavior can be overridden by setting `ignore` against a field.\n   *\n   * ```proto\n   * message MyMessage {\n   *   // Only one of `field1` or `field2` _can_ be present in this message.\n   *   option (buf.validate.message).oneof = { fields: [\"field1\", \"field2\"] };\n   *   // Exactly one of `field3` or `field4` _must_ be present in this message.\n   *   option (buf.validate.message).oneof = { fields: [\"field3\", \"field4\"], required: true };\n   *   string field1 = 1;\n   *   bytes field2 = 2;\n   *   bool field3 = 3;\n   *   int32 field4 = 4;\n   * }\n   * ```\n   *\n   * @generated from field: repeated buf.validate.MessageOneofRule oneof = 4;\n   */\n  oneof: MessageOneofRule[];\n};\n\n/**\n * Describes the message buf.validate.MessageRules.\n * Use `create(MessageRulesSchema)` to create a new message.\n */\nexport const MessageRulesSchema: GenMessage<MessageRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 1);\n\n/**\n * @generated from message buf.validate.MessageOneofRule\n */\nexport type MessageOneofRule = Message<\"buf.validate.MessageOneofRule\"> & {\n  /**\n   * A list of field names to include in the oneof. All field names must be\n   * defined in the message. At least one field must be specified, and\n   * duplicates are not permitted.\n   *\n   * @generated from field: repeated string fields = 1;\n   */\n  fields: string[];\n\n  /**\n   * If true, one of the fields specified _must_ be set.\n   *\n   * @generated from field: optional bool required = 2;\n   */\n  required: boolean;\n};\n\n/**\n * Describes the message buf.validate.MessageOneofRule.\n * Use `create(MessageOneofRuleSchema)` to create a new message.\n */\nexport const MessageOneofRuleSchema: GenMessage<MessageOneofRule> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 2);\n\n/**\n * The `OneofRules` message type enables you to manage rules for\n * oneof fields in your protobuf messages.\n *\n * @generated from message buf.validate.OneofRules\n */\nexport type OneofRules = Message<\"buf.validate.OneofRules\"> & {\n  /**\n   * If `required` is true, exactly one field of the oneof must be set. A\n   * validation error is returned if no fields in the oneof are set. Further rules\n   * should be placed on the fields themselves to ensure they are valid values,\n   * such as `min_len` or `gt`.\n   *\n   * ```proto\n   * message MyMessage {\n   *   oneof value {\n   *     // Either `a` or `b` must be set. If `a` is set, it must also be\n   *     // non-empty; whereas if `b` is set, it can still be an empty string.\n   *     option (buf.validate.oneof).required = true;\n   *     string a = 1 [(buf.validate.field).string.min_len = 1];\n   *     string b = 2;\n   *   }\n   * }\n   * ```\n   *\n   * @generated from field: optional bool required = 1;\n   */\n  required: boolean;\n};\n\n/**\n * Describes the message buf.validate.OneofRules.\n * Use `create(OneofRulesSchema)` to create a new message.\n */\nexport const OneofRulesSchema: GenMessage<OneofRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 3);\n\n/**\n * FieldRules encapsulates the rules for each type of field. Depending on\n * the field, the correct set should be used to ensure proper validations.\n *\n * @generated from message buf.validate.FieldRules\n */\nexport type FieldRules = Message<\"buf.validate.FieldRules\"> & {\n  /**\n   * `cel` is a repeated field used to represent a textual expression\n   * in the Common Expression Language (CEL) syntax. For more information,\n   * [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).\n   *\n   * ```proto\n   * message MyMessage {\n   *   // The field `value` must be greater than 42.\n   *   optional int32 value = 1 [(buf.validate.field).cel = {\n   *     id: \"my_message.value\",\n   *     message: \"value must be greater than 42\",\n   *     expression: \"this > 42\",\n   *   }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated buf.validate.Rule cel = 23;\n   */\n  cel: Rule[];\n\n  /**\n   * If `required` is true, the field must be set. A validation error is returned\n   * if the field is not set.\n   *\n   * ```proto\n   * syntax=\"proto3\";\n   *\n   * message FieldsWithPresence {\n   *   // Requires any string to be set, including the empty string.\n   *   optional string link = 1 [\n   *     (buf.validate.field).required = true\n   *   ];\n   *   // Requires true or false to be set.\n   *   optional bool disabled = 2 [\n   *     (buf.validate.field).required = true\n   *   ];\n   *   // Requires a message to be set, including the empty message.\n   *   SomeMessage msg = 4 [\n   *     (buf.validate.field).required = true\n   *   ];\n   * }\n   * ```\n   *\n   * All fields in the example above track presence. By default, Protovalidate\n   * ignores rules on those fields if no value is set. `required` ensures that\n   * the fields are set and valid.\n   *\n   * Fields that don't track presence are always validated by Protovalidate,\n   * whether they are set or not. It is not necessary to add `required`. It\n   * can be added to indicate that the field cannot be the zero value.\n   *\n   * ```proto\n   * syntax=\"proto3\";\n   *\n   * message FieldsWithoutPresence {\n   *   // `string.email` always applies, even to an empty string.\n   *   string link = 1 [\n   *     (buf.validate.field).string.email = true\n   *   ];\n   *   // `repeated.min_items` always applies, even to an empty list.\n   *   repeated string labels = 2 [\n   *     (buf.validate.field).repeated.min_items = 1\n   *   ];\n   *   // `required`, for fields that don't track presence, indicates\n   *   // the value of the field can't be the zero value.\n   *   int32 zero_value_not_allowed = 3 [\n   *     (buf.validate.field).required = true\n   *   ];\n   * }\n   * ```\n   *\n   * To learn which fields track presence, see the\n   * [Field Presence cheat sheet](https://protobuf.dev/programming-guides/field_presence/#cheat).\n   *\n   * Note: While field rules can be applied to repeated items, map keys, and map\n   * values, the elements are always considered to be set. Consequently,\n   * specifying `repeated.items.required` is redundant.\n   *\n   * @generated from field: optional bool required = 25;\n   */\n  required: boolean;\n\n  /**\n   * Ignore validation rules on the field if its value matches the specified\n   * criteria. See the `Ignore` enum for details.\n   *\n   * ```proto\n   * message UpdateRequest {\n   *   // The uri rule only applies if the field is not an empty string.\n   *   string url = 1 [\n   *     (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE,\n   *     (buf.validate.field).string.uri = true\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: optional buf.validate.Ignore ignore = 27;\n   */\n  ignore: Ignore;\n\n  /**\n   * @generated from oneof buf.validate.FieldRules.type\n   */\n  type: {\n    /**\n     * Scalar Field Types\n     *\n     * @generated from field: buf.validate.FloatRules float = 1;\n     */\n    value: FloatRules;\n    case: \"float\";\n  } | {\n    /**\n     * @generated from field: buf.validate.DoubleRules double = 2;\n     */\n    value: DoubleRules;\n    case: \"double\";\n  } | {\n    /**\n     * @generated from field: buf.validate.Int32Rules int32 = 3;\n     */\n    value: Int32Rules;\n    case: \"int32\";\n  } | {\n    /**\n     * @generated from field: buf.validate.Int64Rules int64 = 4;\n     */\n    value: Int64Rules;\n    case: \"int64\";\n  } | {\n    /**\n     * @generated from field: buf.validate.UInt32Rules uint32 = 5;\n     */\n    value: UInt32Rules;\n    case: \"uint32\";\n  } | {\n    /**\n     * @generated from field: buf.validate.UInt64Rules uint64 = 6;\n     */\n    value: UInt64Rules;\n    case: \"uint64\";\n  } | {\n    /**\n     * @generated from field: buf.validate.SInt32Rules sint32 = 7;\n     */\n    value: SInt32Rules;\n    case: \"sint32\";\n  } | {\n    /**\n     * @generated from field: buf.validate.SInt64Rules sint64 = 8;\n     */\n    value: SInt64Rules;\n    case: \"sint64\";\n  } | {\n    /**\n     * @generated from field: buf.validate.Fixed32Rules fixed32 = 9;\n     */\n    value: Fixed32Rules;\n    case: \"fixed32\";\n  } | {\n    /**\n     * @generated from field: buf.validate.Fixed64Rules fixed64 = 10;\n     */\n    value: Fixed64Rules;\n    case: \"fixed64\";\n  } | {\n    /**\n     * @generated from field: buf.validate.SFixed32Rules sfixed32 = 11;\n     */\n    value: SFixed32Rules;\n    case: \"sfixed32\";\n  } | {\n    /**\n     * @generated from field: buf.validate.SFixed64Rules sfixed64 = 12;\n     */\n    value: SFixed64Rules;\n    case: \"sfixed64\";\n  } | {\n    /**\n     * @generated from field: buf.validate.BoolRules bool = 13;\n     */\n    value: BoolRules;\n    case: \"bool\";\n  } | {\n    /**\n     * @generated from field: buf.validate.StringRules string = 14;\n     */\n    value: StringRules;\n    case: \"string\";\n  } | {\n    /**\n     * @generated from field: buf.validate.BytesRules bytes = 15;\n     */\n    value: BytesRules;\n    case: \"bytes\";\n  } | {\n    /**\n     * Complex Field Types\n     *\n     * @generated from field: buf.validate.EnumRules enum = 16;\n     */\n    value: EnumRules;\n    case: \"enum\";\n  } | {\n    /**\n     * @generated from field: buf.validate.RepeatedRules repeated = 18;\n     */\n    value: RepeatedRules;\n    case: \"repeated\";\n  } | {\n    /**\n     * @generated from field: buf.validate.MapRules map = 19;\n     */\n    value: MapRules;\n    case: \"map\";\n  } | {\n    /**\n     * Well-Known Field Types\n     *\n     * @generated from field: buf.validate.AnyRules any = 20;\n     */\n    value: AnyRules;\n    case: \"any\";\n  } | {\n    /**\n     * @generated from field: buf.validate.DurationRules duration = 21;\n     */\n    value: DurationRules;\n    case: \"duration\";\n  } | {\n    /**\n     * @generated from field: buf.validate.TimestampRules timestamp = 22;\n     */\n    value: TimestampRules;\n    case: \"timestamp\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message buf.validate.FieldRules.\n * Use `create(FieldRulesSchema)` to create a new message.\n */\nexport const FieldRulesSchema: GenMessage<FieldRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 4);\n\n/**\n * PredefinedRules are custom rules that can be re-used with\n * multiple fields.\n *\n * @generated from message buf.validate.PredefinedRules\n */\nexport type PredefinedRules = Message<\"buf.validate.PredefinedRules\"> & {\n  /**\n   * `cel` is a repeated field used to represent a textual expression\n   * in the Common Expression Language (CEL) syntax. For more information,\n   * [see our documentation](https://buf.build/docs/protovalidate/schemas/predefined-rules/).\n   *\n   * ```proto\n   * message MyMessage {\n   *   // The field `value` must be greater than 42.\n   *   optional int32 value = 1 [(buf.validate.predefined).cel = {\n   *     id: \"my_message.value\",\n   *     message: \"value must be greater than 42\",\n   *     expression: \"this > 42\",\n   *   }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated buf.validate.Rule cel = 1;\n   */\n  cel: Rule[];\n};\n\n/**\n * Describes the message buf.validate.PredefinedRules.\n * Use `create(PredefinedRulesSchema)` to create a new message.\n */\nexport const PredefinedRulesSchema: GenMessage<PredefinedRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 5);\n\n/**\n * FloatRules describes the rules applied to `float` values. These\n * rules may also be applied to the `google.protobuf.FloatValue` Well-Known-Type.\n *\n * @generated from message buf.validate.FloatRules\n */\nexport type FloatRules = Message<\"buf.validate.FloatRules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyFloat {\n   *   // value must equal 42.0\n   *   float value = 1 [(buf.validate.field).float.const = 42.0];\n   * }\n   * ```\n   *\n   * @generated from field: optional float const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.FloatRules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFloat {\n     *   // value must be less than 10.0\n     *   float value = 1 [(buf.validate.field).float.lt = 10.0];\n     * }\n     * ```\n     *\n     * @generated from field: float lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyFloat {\n     *   // value must be less than or equal to 10.0\n     *   float value = 1 [(buf.validate.field).float.lte = 10.0];\n     * }\n     * ```\n     *\n     * @generated from field: float lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.FloatRules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFloat {\n     *   // value must be greater than 5.0 [float.gt]\n     *   float value = 1 [(buf.validate.field).float.gt = 5.0];\n     *\n     *   // value must be greater than 5 and less than 10.0 [float.gt_lt]\n     *   float other_value = 2 [(buf.validate.field).float = { gt: 5.0, lt: 10.0 }];\n     *\n     *   // value must be greater than 10 or less than 5.0 [float.gt_lt_exclusive]\n     *   float another_value = 3 [(buf.validate.field).float = { gt: 10.0, lt: 5.0 }];\n     * }\n     * ```\n     *\n     * @generated from field: float gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFloat {\n     *   // value must be greater than or equal to 5.0 [float.gte]\n     *   float value = 1 [(buf.validate.field).float.gte = 5.0];\n     *\n     *   // value must be greater than or equal to 5.0 and less than 10.0 [float.gte_lt]\n     *   float other_value = 2 [(buf.validate.field).float = { gte: 5.0, lt: 10.0 }];\n     *\n     *   // value must be greater than or equal to 10.0 or less than 5.0 [float.gte_lt_exclusive]\n     *   float another_value = 3 [(buf.validate.field).float = { gte: 10.0, lt: 5.0 }];\n     * }\n     * ```\n     *\n     * @generated from field: float gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message\n   * is generated.\n   *\n   * ```proto\n   * message MyFloat {\n   *   // value must be in list [1.0, 2.0, 3.0]\n   *   float value = 1 [(buf.validate.field).float = { in: [1.0, 2.0, 3.0] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated float in = 6;\n   */\n  in: number[];\n\n  /**\n   * `in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyFloat {\n   *   // value must not be in list [1.0, 2.0, 3.0]\n   *   float value = 1 [(buf.validate.field).float = { not_in: [1.0, 2.0, 3.0] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated float not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `finite` requires the field value to be finite. If the field value is\n   * infinite or NaN, an error message is generated.\n   *\n   * @generated from field: optional bool finite = 8;\n   */\n  finite: boolean;\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyFloat {\n   *   float value = 1 [\n   *     (buf.validate.field).float.example = 1.0,\n   *     (buf.validate.field).float.example = inf\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated float example = 9;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.FloatRules.\n * Use `create(FloatRulesSchema)` to create a new message.\n */\nexport const FloatRulesSchema: GenMessage<FloatRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 6);\n\n/**\n * DoubleRules describes the rules applied to `double` values. These\n * rules may also be applied to the `google.protobuf.DoubleValue` Well-Known-Type.\n *\n * @generated from message buf.validate.DoubleRules\n */\nexport type DoubleRules = Message<\"buf.validate.DoubleRules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyDouble {\n   *   // value must equal 42.0\n   *   double value = 1 [(buf.validate.field).double.const = 42.0];\n   * }\n   * ```\n   *\n   * @generated from field: optional double const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.DoubleRules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyDouble {\n     *   // value must be less than 10.0\n     *   double value = 1 [(buf.validate.field).double.lt = 10.0];\n     * }\n     * ```\n     *\n     * @generated from field: double lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified value\n     * (field <= value). If the field value is greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyDouble {\n     *   // value must be less than or equal to 10.0\n     *   double value = 1 [(buf.validate.field).double.lte = 10.0];\n     * }\n     * ```\n     *\n     * @generated from field: double lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.DoubleRules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or `lte`,\n     * the range is reversed, and the field value must be outside the specified\n     * range. If the field value doesn't meet the required conditions, an error\n     * message is generated.\n     *\n     * ```proto\n     * message MyDouble {\n     *   // value must be greater than 5.0 [double.gt]\n     *   double value = 1 [(buf.validate.field).double.gt = 5.0];\n     *\n     *   // value must be greater than 5 and less than 10.0 [double.gt_lt]\n     *   double other_value = 2 [(buf.validate.field).double = { gt: 5.0, lt: 10.0 }];\n     *\n     *   // value must be greater than 10 or less than 5.0 [double.gt_lt_exclusive]\n     *   double another_value = 3 [(buf.validate.field).double = { gt: 10.0, lt: 5.0 }];\n     * }\n     * ```\n     *\n     * @generated from field: double gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyDouble {\n     *   // value must be greater than or equal to 5.0 [double.gte]\n     *   double value = 1 [(buf.validate.field).double.gte = 5.0];\n     *\n     *   // value must be greater than or equal to 5.0 and less than 10.0 [double.gte_lt]\n     *   double other_value = 2 [(buf.validate.field).double = { gte: 5.0, lt: 10.0 }];\n     *\n     *   // value must be greater than or equal to 10.0 or less than 5.0 [double.gte_lt_exclusive]\n     *   double another_value = 3 [(buf.validate.field).double = { gte: 10.0, lt: 5.0 }];\n     * }\n     * ```\n     *\n     * @generated from field: double gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyDouble {\n   *   // value must be in list [1.0, 2.0, 3.0]\n   *   double value = 1 [(buf.validate.field).double = { in: [1.0, 2.0, 3.0] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated double in = 6;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyDouble {\n   *   // value must not be in list [1.0, 2.0, 3.0]\n   *   double value = 1 [(buf.validate.field).double = { not_in: [1.0, 2.0, 3.0] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated double not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `finite` requires the field value to be finite. If the field value is\n   * infinite or NaN, an error message is generated.\n   *\n   * @generated from field: optional bool finite = 8;\n   */\n  finite: boolean;\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyDouble {\n   *   double value = 1 [\n   *     (buf.validate.field).double.example = 1.0,\n   *     (buf.validate.field).double.example = inf\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated double example = 9;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.DoubleRules.\n * Use `create(DoubleRulesSchema)` to create a new message.\n */\nexport const DoubleRulesSchema: GenMessage<DoubleRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 7);\n\n/**\n * Int32Rules describes the rules applied to `int32` values. These\n * rules may also be applied to the `google.protobuf.Int32Value` Well-Known-Type.\n *\n * @generated from message buf.validate.Int32Rules\n */\nexport type Int32Rules = Message<\"buf.validate.Int32Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyInt32 {\n   *   // value must equal 42\n   *   int32 value = 1 [(buf.validate.field).int32.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional int32 const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.Int32Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field\n     * < value). If the field value is equal to or greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyInt32 {\n     *   // value must be less than 10\n     *   int32 value = 1 [(buf.validate.field).int32.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: int32 lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyInt32 {\n     *   // value must be less than or equal to 10\n     *   int32 value = 1 [(buf.validate.field).int32.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: int32 lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.Int32Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyInt32 {\n     *   // value must be greater than 5 [int32.gt]\n     *   int32 value = 1 [(buf.validate.field).int32.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [int32.gt_lt]\n     *   int32 other_value = 2 [(buf.validate.field).int32 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [int32.gt_lt_exclusive]\n     *   int32 another_value = 3 [(buf.validate.field).int32 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: int32 gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified value\n     * (exclusive). If the value of `gte` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyInt32 {\n     *   // value must be greater than or equal to 5 [int32.gte]\n     *   int32 value = 1 [(buf.validate.field).int32.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [int32.gte_lt]\n     *   int32 other_value = 2 [(buf.validate.field).int32 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [int32.gte_lt_exclusive]\n     *   int32 another_value = 3 [(buf.validate.field).int32 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: int32 gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyInt32 {\n   *   // value must be in list [1, 2, 3]\n   *   int32 value = 1 [(buf.validate.field).int32 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int32 in = 6;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error message\n   * is generated.\n   *\n   * ```proto\n   * message MyInt32 {\n   *   // value must not be in list [1, 2, 3]\n   *   int32 value = 1 [(buf.validate.field).int32 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int32 not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyInt32 {\n   *   int32 value = 1 [\n   *     (buf.validate.field).int32.example = 1,\n   *     (buf.validate.field).int32.example = -10\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int32 example = 8;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.Int32Rules.\n * Use `create(Int32RulesSchema)` to create a new message.\n */\nexport const Int32RulesSchema: GenMessage<Int32Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 8);\n\n/**\n * Int64Rules describes the rules applied to `int64` values. These\n * rules may also be applied to the `google.protobuf.Int64Value` Well-Known-Type.\n *\n * @generated from message buf.validate.Int64Rules\n */\nexport type Int64Rules = Message<\"buf.validate.Int64Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyInt64 {\n   *   // value must equal 42\n   *   int64 value = 1 [(buf.validate.field).int64.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional int64 const = 1;\n   */\n  const: bigint;\n\n  /**\n   * @generated from oneof buf.validate.Int64Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyInt64 {\n     *   // value must be less than 10\n     *   int64 value = 1 [(buf.validate.field).int64.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: int64 lt = 2;\n     */\n    value: bigint;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyInt64 {\n     *   // value must be less than or equal to 10\n     *   int64 value = 1 [(buf.validate.field).int64.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: int64 lte = 3;\n     */\n    value: bigint;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.Int64Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyInt64 {\n     *   // value must be greater than 5 [int64.gt]\n     *   int64 value = 1 [(buf.validate.field).int64.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [int64.gt_lt]\n     *   int64 other_value = 2 [(buf.validate.field).int64 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [int64.gt_lt_exclusive]\n     *   int64 another_value = 3 [(buf.validate.field).int64 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: int64 gt = 4;\n     */\n    value: bigint;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyInt64 {\n     *   // value must be greater than or equal to 5 [int64.gte]\n     *   int64 value = 1 [(buf.validate.field).int64.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [int64.gte_lt]\n     *   int64 other_value = 2 [(buf.validate.field).int64 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [int64.gte_lt_exclusive]\n     *   int64 another_value = 3 [(buf.validate.field).int64 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: int64 gte = 5;\n     */\n    value: bigint;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyInt64 {\n   *   // value must be in list [1, 2, 3]\n   *   int64 value = 1 [(buf.validate.field).int64 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int64 in = 6;\n   */\n  in: bigint[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyInt64 {\n   *   // value must not be in list [1, 2, 3]\n   *   int64 value = 1 [(buf.validate.field).int64 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int64 not_in = 7;\n   */\n  notIn: bigint[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyInt64 {\n   *   int64 value = 1 [\n   *     (buf.validate.field).int64.example = 1,\n   *     (buf.validate.field).int64.example = -10\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int64 example = 9;\n   */\n  example: bigint[];\n};\n\n/**\n * Describes the message buf.validate.Int64Rules.\n * Use `create(Int64RulesSchema)` to create a new message.\n */\nexport const Int64RulesSchema: GenMessage<Int64Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 9);\n\n/**\n * UInt32Rules describes the rules applied to `uint32` values. These\n * rules may also be applied to the `google.protobuf.UInt32Value` Well-Known-Type.\n *\n * @generated from message buf.validate.UInt32Rules\n */\nexport type UInt32Rules = Message<\"buf.validate.UInt32Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyUInt32 {\n   *   // value must equal 42\n   *   uint32 value = 1 [(buf.validate.field).uint32.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint32 const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.UInt32Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyUInt32 {\n     *   // value must be less than 10\n     *   uint32 value = 1 [(buf.validate.field).uint32.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: uint32 lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyUInt32 {\n     *   // value must be less than or equal to 10\n     *   uint32 value = 1 [(buf.validate.field).uint32.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: uint32 lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.UInt32Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyUInt32 {\n     *   // value must be greater than 5 [uint32.gt]\n     *   uint32 value = 1 [(buf.validate.field).uint32.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [uint32.gt_lt]\n     *   uint32 other_value = 2 [(buf.validate.field).uint32 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [uint32.gt_lt_exclusive]\n     *   uint32 another_value = 3 [(buf.validate.field).uint32 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: uint32 gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyUInt32 {\n     *   // value must be greater than or equal to 5 [uint32.gte]\n     *   uint32 value = 1 [(buf.validate.field).uint32.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [uint32.gte_lt]\n     *   uint32 other_value = 2 [(buf.validate.field).uint32 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [uint32.gte_lt_exclusive]\n     *   uint32 another_value = 3 [(buf.validate.field).uint32 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: uint32 gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyUInt32 {\n   *   // value must be in list [1, 2, 3]\n   *   uint32 value = 1 [(buf.validate.field).uint32 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated uint32 in = 6;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyUInt32 {\n   *   // value must not be in list [1, 2, 3]\n   *   uint32 value = 1 [(buf.validate.field).uint32 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated uint32 not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyUInt32 {\n   *   uint32 value = 1 [\n   *     (buf.validate.field).uint32.example = 1,\n   *     (buf.validate.field).uint32.example = 10\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated uint32 example = 8;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.UInt32Rules.\n * Use `create(UInt32RulesSchema)` to create a new message.\n */\nexport const UInt32RulesSchema: GenMessage<UInt32Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 10);\n\n/**\n * UInt64Rules describes the rules applied to `uint64` values. These\n * rules may also be applied to the `google.protobuf.UInt64Value` Well-Known-Type.\n *\n * @generated from message buf.validate.UInt64Rules\n */\nexport type UInt64Rules = Message<\"buf.validate.UInt64Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyUInt64 {\n   *   // value must equal 42\n   *   uint64 value = 1 [(buf.validate.field).uint64.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 const = 1;\n   */\n  const: bigint;\n\n  /**\n   * @generated from oneof buf.validate.UInt64Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyUInt64 {\n     *   // value must be less than 10\n     *   uint64 value = 1 [(buf.validate.field).uint64.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: uint64 lt = 2;\n     */\n    value: bigint;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyUInt64 {\n     *   // value must be less than or equal to 10\n     *   uint64 value = 1 [(buf.validate.field).uint64.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: uint64 lte = 3;\n     */\n    value: bigint;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.UInt64Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyUInt64 {\n     *   // value must be greater than 5 [uint64.gt]\n     *   uint64 value = 1 [(buf.validate.field).uint64.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [uint64.gt_lt]\n     *   uint64 other_value = 2 [(buf.validate.field).uint64 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [uint64.gt_lt_exclusive]\n     *   uint64 another_value = 3 [(buf.validate.field).uint64 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: uint64 gt = 4;\n     */\n    value: bigint;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyUInt64 {\n     *   // value must be greater than or equal to 5 [uint64.gte]\n     *   uint64 value = 1 [(buf.validate.field).uint64.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [uint64.gte_lt]\n     *   uint64 other_value = 2 [(buf.validate.field).uint64 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [uint64.gte_lt_exclusive]\n     *   uint64 another_value = 3 [(buf.validate.field).uint64 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: uint64 gte = 5;\n     */\n    value: bigint;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyUInt64 {\n   *   // value must be in list [1, 2, 3]\n   *   uint64 value = 1 [(buf.validate.field).uint64 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated uint64 in = 6;\n   */\n  in: bigint[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyUInt64 {\n   *   // value must not be in list [1, 2, 3]\n   *   uint64 value = 1 [(buf.validate.field).uint64 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated uint64 not_in = 7;\n   */\n  notIn: bigint[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyUInt64 {\n   *   uint64 value = 1 [\n   *     (buf.validate.field).uint64.example = 1,\n   *     (buf.validate.field).uint64.example = -10\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated uint64 example = 8;\n   */\n  example: bigint[];\n};\n\n/**\n * Describes the message buf.validate.UInt64Rules.\n * Use `create(UInt64RulesSchema)` to create a new message.\n */\nexport const UInt64RulesSchema: GenMessage<UInt64Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 11);\n\n/**\n * SInt32Rules describes the rules applied to `sint32` values.\n *\n * @generated from message buf.validate.SInt32Rules\n */\nexport type SInt32Rules = Message<\"buf.validate.SInt32Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MySInt32 {\n   *   // value must equal 42\n   *   sint32 value = 1 [(buf.validate.field).sint32.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional sint32 const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.SInt32Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field\n     * < value). If the field value is equal to or greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MySInt32 {\n     *   // value must be less than 10\n     *   sint32 value = 1 [(buf.validate.field).sint32.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sint32 lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MySInt32 {\n     *   // value must be less than or equal to 10\n     *   sint32 value = 1 [(buf.validate.field).sint32.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sint32 lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.SInt32Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySInt32 {\n     *   // value must be greater than 5 [sint32.gt]\n     *   sint32 value = 1 [(buf.validate.field).sint32.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [sint32.gt_lt]\n     *   sint32 other_value = 2 [(buf.validate.field).sint32 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [sint32.gt_lt_exclusive]\n     *   sint32 another_value = 3 [(buf.validate.field).sint32 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sint32 gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySInt32 {\n     *  // value must be greater than or equal to 5 [sint32.gte]\n     *  sint32 value = 1 [(buf.validate.field).sint32.gte = 5];\n     *\n     *  // value must be greater than or equal to 5 and less than 10 [sint32.gte_lt]\n     *  sint32 other_value = 2 [(buf.validate.field).sint32 = { gte: 5, lt: 10 }];\n     *\n     *  // value must be greater than or equal to 10 or less than 5 [sint32.gte_lt_exclusive]\n     *  sint32 another_value = 3 [(buf.validate.field).sint32 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sint32 gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MySInt32 {\n   *   // value must be in list [1, 2, 3]\n   *   sint32 value = 1 [(buf.validate.field).sint32 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sint32 in = 6;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MySInt32 {\n   *   // value must not be in list [1, 2, 3]\n   *   sint32 value = 1 [(buf.validate.field).sint32 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sint32 not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MySInt32 {\n   *   sint32 value = 1 [\n   *     (buf.validate.field).sint32.example = 1,\n   *     (buf.validate.field).sint32.example = -10\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sint32 example = 8;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.SInt32Rules.\n * Use `create(SInt32RulesSchema)` to create a new message.\n */\nexport const SInt32RulesSchema: GenMessage<SInt32Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 12);\n\n/**\n * SInt64Rules describes the rules applied to `sint64` values.\n *\n * @generated from message buf.validate.SInt64Rules\n */\nexport type SInt64Rules = Message<\"buf.validate.SInt64Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MySInt64 {\n   *   // value must equal 42\n   *   sint64 value = 1 [(buf.validate.field).sint64.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional sint64 const = 1;\n   */\n  const: bigint;\n\n  /**\n   * @generated from oneof buf.validate.SInt64Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field\n     * < value). If the field value is equal to or greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MySInt64 {\n     *   // value must be less than 10\n     *   sint64 value = 1 [(buf.validate.field).sint64.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sint64 lt = 2;\n     */\n    value: bigint;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MySInt64 {\n     *   // value must be less than or equal to 10\n     *   sint64 value = 1 [(buf.validate.field).sint64.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sint64 lte = 3;\n     */\n    value: bigint;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.SInt64Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySInt64 {\n     *   // value must be greater than 5 [sint64.gt]\n     *   sint64 value = 1 [(buf.validate.field).sint64.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [sint64.gt_lt]\n     *   sint64 other_value = 2 [(buf.validate.field).sint64 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [sint64.gt_lt_exclusive]\n     *   sint64 another_value = 3 [(buf.validate.field).sint64 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sint64 gt = 4;\n     */\n    value: bigint;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySInt64 {\n     *   // value must be greater than or equal to 5 [sint64.gte]\n     *   sint64 value = 1 [(buf.validate.field).sint64.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [sint64.gte_lt]\n     *   sint64 other_value = 2 [(buf.validate.field).sint64 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [sint64.gte_lt_exclusive]\n     *   sint64 another_value = 3 [(buf.validate.field).sint64 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sint64 gte = 5;\n     */\n    value: bigint;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message\n   * is generated.\n   *\n   * ```proto\n   * message MySInt64 {\n   *   // value must be in list [1, 2, 3]\n   *   sint64 value = 1 [(buf.validate.field).sint64 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sint64 in = 6;\n   */\n  in: bigint[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MySInt64 {\n   *   // value must not be in list [1, 2, 3]\n   *   sint64 value = 1 [(buf.validate.field).sint64 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sint64 not_in = 7;\n   */\n  notIn: bigint[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MySInt64 {\n   *   sint64 value = 1 [\n   *     (buf.validate.field).sint64.example = 1,\n   *     (buf.validate.field).sint64.example = -10\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sint64 example = 8;\n   */\n  example: bigint[];\n};\n\n/**\n * Describes the message buf.validate.SInt64Rules.\n * Use `create(SInt64RulesSchema)` to create a new message.\n */\nexport const SInt64RulesSchema: GenMessage<SInt64Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 13);\n\n/**\n * Fixed32Rules describes the rules applied to `fixed32` values.\n *\n * @generated from message buf.validate.Fixed32Rules\n */\nexport type Fixed32Rules = Message<\"buf.validate.Fixed32Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value.\n   * If the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyFixed32 {\n   *   // value must equal 42\n   *   fixed32 value = 1 [(buf.validate.field).fixed32.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional fixed32 const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.Fixed32Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFixed32 {\n     *   // value must be less than 10\n     *   fixed32 value = 1 [(buf.validate.field).fixed32.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: fixed32 lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyFixed32 {\n     *   // value must be less than or equal to 10\n     *   fixed32 value = 1 [(buf.validate.field).fixed32.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: fixed32 lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.Fixed32Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFixed32 {\n     *   // value must be greater than 5 [fixed32.gt]\n     *   fixed32 value = 1 [(buf.validate.field).fixed32.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [fixed32.gt_lt]\n     *   fixed32 other_value = 2 [(buf.validate.field).fixed32 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [fixed32.gt_lt_exclusive]\n     *   fixed32 another_value = 3 [(buf.validate.field).fixed32 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: fixed32 gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFixed32 {\n     *   // value must be greater than or equal to 5 [fixed32.gte]\n     *   fixed32 value = 1 [(buf.validate.field).fixed32.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [fixed32.gte_lt]\n     *   fixed32 other_value = 2 [(buf.validate.field).fixed32 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [fixed32.gte_lt_exclusive]\n     *   fixed32 another_value = 3 [(buf.validate.field).fixed32 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: fixed32 gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message\n   * is generated.\n   *\n   * ```proto\n   * message MyFixed32 {\n   *   // value must be in list [1, 2, 3]\n   *   fixed32 value = 1 [(buf.validate.field).fixed32 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated fixed32 in = 6;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyFixed32 {\n   *   // value must not be in list [1, 2, 3]\n   *   fixed32 value = 1 [(buf.validate.field).fixed32 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated fixed32 not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyFixed32 {\n   *   fixed32 value = 1 [\n   *     (buf.validate.field).fixed32.example = 1,\n   *     (buf.validate.field).fixed32.example = 2\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated fixed32 example = 8;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.Fixed32Rules.\n * Use `create(Fixed32RulesSchema)` to create a new message.\n */\nexport const Fixed32RulesSchema: GenMessage<Fixed32Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 14);\n\n/**\n * Fixed64Rules describes the rules applied to `fixed64` values.\n *\n * @generated from message buf.validate.Fixed64Rules\n */\nexport type Fixed64Rules = Message<\"buf.validate.Fixed64Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyFixed64 {\n   *   // value must equal 42\n   *   fixed64 value = 1 [(buf.validate.field).fixed64.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional fixed64 const = 1;\n   */\n  const: bigint;\n\n  /**\n   * @generated from oneof buf.validate.Fixed64Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFixed64 {\n     *   // value must be less than 10\n     *   fixed64 value = 1 [(buf.validate.field).fixed64.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: fixed64 lt = 2;\n     */\n    value: bigint;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MyFixed64 {\n     *   // value must be less than or equal to 10\n     *   fixed64 value = 1 [(buf.validate.field).fixed64.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: fixed64 lte = 3;\n     */\n    value: bigint;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.Fixed64Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFixed64 {\n     *   // value must be greater than 5 [fixed64.gt]\n     *   fixed64 value = 1 [(buf.validate.field).fixed64.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [fixed64.gt_lt]\n     *   fixed64 other_value = 2 [(buf.validate.field).fixed64 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [fixed64.gt_lt_exclusive]\n     *   fixed64 another_value = 3 [(buf.validate.field).fixed64 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: fixed64 gt = 4;\n     */\n    value: bigint;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyFixed64 {\n     *   // value must be greater than or equal to 5 [fixed64.gte]\n     *   fixed64 value = 1 [(buf.validate.field).fixed64.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [fixed64.gte_lt]\n     *   fixed64 other_value = 2 [(buf.validate.field).fixed64 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [fixed64.gte_lt_exclusive]\n     *   fixed64 another_value = 3 [(buf.validate.field).fixed64 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: fixed64 gte = 5;\n     */\n    value: bigint;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyFixed64 {\n   *   // value must be in list [1, 2, 3]\n   *   fixed64 value = 1 [(buf.validate.field).fixed64 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated fixed64 in = 6;\n   */\n  in: bigint[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyFixed64 {\n   *   // value must not be in list [1, 2, 3]\n   *   fixed64 value = 1 [(buf.validate.field).fixed64 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated fixed64 not_in = 7;\n   */\n  notIn: bigint[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyFixed64 {\n   *   fixed64 value = 1 [\n   *     (buf.validate.field).fixed64.example = 1,\n   *     (buf.validate.field).fixed64.example = 2\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated fixed64 example = 8;\n   */\n  example: bigint[];\n};\n\n/**\n * Describes the message buf.validate.Fixed64Rules.\n * Use `create(Fixed64RulesSchema)` to create a new message.\n */\nexport const Fixed64RulesSchema: GenMessage<Fixed64Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 15);\n\n/**\n * SFixed32Rules describes the rules applied to `fixed32` values.\n *\n * @generated from message buf.validate.SFixed32Rules\n */\nexport type SFixed32Rules = Message<\"buf.validate.SFixed32Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MySFixed32 {\n   *   // value must equal 42\n   *   sfixed32 value = 1 [(buf.validate.field).sfixed32.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional sfixed32 const = 1;\n   */\n  const: number;\n\n  /**\n   * @generated from oneof buf.validate.SFixed32Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySFixed32 {\n     *   // value must be less than 10\n     *   sfixed32 value = 1 [(buf.validate.field).sfixed32.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed32 lt = 2;\n     */\n    value: number;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MySFixed32 {\n     *   // value must be less than or equal to 10\n     *   sfixed32 value = 1 [(buf.validate.field).sfixed32.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed32 lte = 3;\n     */\n    value: number;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.SFixed32Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySFixed32 {\n     *   // value must be greater than 5 [sfixed32.gt]\n     *   sfixed32 value = 1 [(buf.validate.field).sfixed32.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [sfixed32.gt_lt]\n     *   sfixed32 other_value = 2 [(buf.validate.field).sfixed32 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [sfixed32.gt_lt_exclusive]\n     *   sfixed32 another_value = 3 [(buf.validate.field).sfixed32 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed32 gt = 4;\n     */\n    value: number;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySFixed32 {\n     *   // value must be greater than or equal to 5 [sfixed32.gte]\n     *   sfixed32 value = 1 [(buf.validate.field).sfixed32.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [sfixed32.gte_lt]\n     *   sfixed32 other_value = 2 [(buf.validate.field).sfixed32 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [sfixed32.gte_lt_exclusive]\n     *   sfixed32 another_value = 3 [(buf.validate.field).sfixed32 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed32 gte = 5;\n     */\n    value: number;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MySFixed32 {\n   *   // value must be in list [1, 2, 3]\n   *   sfixed32 value = 1 [(buf.validate.field).sfixed32 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sfixed32 in = 6;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MySFixed32 {\n   *   // value must not be in list [1, 2, 3]\n   *   sfixed32 value = 1 [(buf.validate.field).sfixed32 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sfixed32 not_in = 7;\n   */\n  notIn: number[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MySFixed32 {\n   *   sfixed32 value = 1 [\n   *     (buf.validate.field).sfixed32.example = 1,\n   *     (buf.validate.field).sfixed32.example = 2\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sfixed32 example = 8;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.SFixed32Rules.\n * Use `create(SFixed32RulesSchema)` to create a new message.\n */\nexport const SFixed32RulesSchema: GenMessage<SFixed32Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 16);\n\n/**\n * SFixed64Rules describes the rules applied to `fixed64` values.\n *\n * @generated from message buf.validate.SFixed64Rules\n */\nexport type SFixed64Rules = Message<\"buf.validate.SFixed64Rules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MySFixed64 {\n   *   // value must equal 42\n   *   sfixed64 value = 1 [(buf.validate.field).sfixed64.const = 42];\n   * }\n   * ```\n   *\n   * @generated from field: optional sfixed64 const = 1;\n   */\n  const: bigint;\n\n  /**\n   * @generated from oneof buf.validate.SFixed64Rules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` requires the field value to be less than the specified value (field <\n     * value). If the field value is equal to or greater than the specified value,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySFixed64 {\n     *   // value must be less than 10\n     *   sfixed64 value = 1 [(buf.validate.field).sfixed64.lt = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed64 lt = 2;\n     */\n    value: bigint;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` requires the field value to be less than or equal to the specified\n     * value (field <= value). If the field value is greater than the specified\n     * value, an error message is generated.\n     *\n     * ```proto\n     * message MySFixed64 {\n     *   // value must be less than or equal to 10\n     *   sfixed64 value = 1 [(buf.validate.field).sfixed64.lte = 10];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed64 lte = 3;\n     */\n    value: bigint;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.SFixed64Rules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the field value to be greater than the specified value\n     * (exclusive). If the value of `gt` is larger than a specified `lt` or\n     * `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySFixed64 {\n     *   // value must be greater than 5 [sfixed64.gt]\n     *   sfixed64 value = 1 [(buf.validate.field).sfixed64.gt = 5];\n     *\n     *   // value must be greater than 5 and less than 10 [sfixed64.gt_lt]\n     *   sfixed64 other_value = 2 [(buf.validate.field).sfixed64 = { gt: 5, lt: 10 }];\n     *\n     *   // value must be greater than 10 or less than 5 [sfixed64.gt_lt_exclusive]\n     *   sfixed64 another_value = 3 [(buf.validate.field).sfixed64 = { gt: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed64 gt = 4;\n     */\n    value: bigint;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the field value to be greater than or equal to the specified\n     * value (exclusive). If the value of `gte` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MySFixed64 {\n     *   // value must be greater than or equal to 5 [sfixed64.gte]\n     *   sfixed64 value = 1 [(buf.validate.field).sfixed64.gte = 5];\n     *\n     *   // value must be greater than or equal to 5 and less than 10 [sfixed64.gte_lt]\n     *   sfixed64 other_value = 2 [(buf.validate.field).sfixed64 = { gte: 5, lt: 10 }];\n     *\n     *   // value must be greater than or equal to 10 or less than 5 [sfixed64.gte_lt_exclusive]\n     *   sfixed64 another_value = 3 [(buf.validate.field).sfixed64 = { gte: 10, lt: 5 }];\n     * }\n     * ```\n     *\n     * @generated from field: sfixed64 gte = 5;\n     */\n    value: bigint;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` requires the field value to be equal to one of the specified values.\n   * If the field value isn't one of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MySFixed64 {\n   *   // value must be in list [1, 2, 3]\n   *   sfixed64 value = 1 [(buf.validate.field).sfixed64 = { in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sfixed64 in = 6;\n   */\n  in: bigint[];\n\n  /**\n   * `not_in` requires the field value to not be equal to any of the specified\n   * values. If the field value is one of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MySFixed64 {\n   *   // value must not be in list [1, 2, 3]\n   *   sfixed64 value = 1 [(buf.validate.field).sfixed64 = { not_in: [1, 2, 3] }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sfixed64 not_in = 7;\n   */\n  notIn: bigint[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MySFixed64 {\n   *   sfixed64 value = 1 [\n   *     (buf.validate.field).sfixed64.example = 1,\n   *     (buf.validate.field).sfixed64.example = 2\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated sfixed64 example = 8;\n   */\n  example: bigint[];\n};\n\n/**\n * Describes the message buf.validate.SFixed64Rules.\n * Use `create(SFixed64RulesSchema)` to create a new message.\n */\nexport const SFixed64RulesSchema: GenMessage<SFixed64Rules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 17);\n\n/**\n * BoolRules describes the rules applied to `bool` values. These rules\n * may also be applied to the `google.protobuf.BoolValue` Well-Known-Type.\n *\n * @generated from message buf.validate.BoolRules\n */\nexport type BoolRules = Message<\"buf.validate.BoolRules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified boolean value.\n   * If the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyBool {\n   *   // value must equal true\n   *   bool value = 1 [(buf.validate.field).bool.const = true];\n   * }\n   * ```\n   *\n   * @generated from field: optional bool const = 1;\n   */\n  const: boolean;\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyBool {\n   *   bool value = 1 [\n   *     (buf.validate.field).bool.example = 1,\n   *     (buf.validate.field).bool.example = 2\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated bool example = 2;\n   */\n  example: boolean[];\n};\n\n/**\n * Describes the message buf.validate.BoolRules.\n * Use `create(BoolRulesSchema)` to create a new message.\n */\nexport const BoolRulesSchema: GenMessage<BoolRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 18);\n\n/**\n * StringRules describes the rules applied to `string` values These\n * rules may also be applied to the `google.protobuf.StringValue` Well-Known-Type.\n *\n * @generated from message buf.validate.StringRules\n */\nexport type StringRules = Message<\"buf.validate.StringRules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified value. If\n   * the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value must equal `hello`\n   *   string value = 1 [(buf.validate.field).string.const = \"hello\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string const = 1;\n   */\n  const: string;\n\n  /**\n   * `len` dictates that the field value must have the specified\n   * number of characters (Unicode code points), which may differ from the number\n   * of bytes in the string. If the field value does not meet the specified\n   * length, an error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value length must be 5 characters\n   *   string value = 1 [(buf.validate.field).string.len = 5];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 len = 19;\n   */\n  len: bigint;\n\n  /**\n   * `min_len` specifies that the field value must have at least the specified\n   * number of characters (Unicode code points), which may differ from the number\n   * of bytes in the string. If the field value contains fewer characters, an error\n   * message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value length must be at least 3 characters\n   *   string value = 1 [(buf.validate.field).string.min_len = 3];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 min_len = 2;\n   */\n  minLen: bigint;\n\n  /**\n   * `max_len` specifies that the field value must have no more than the specified\n   * number of characters (Unicode code points), which may differ from the\n   * number of bytes in the string. If the field value contains more characters,\n   * an error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value length must be at most 10 characters\n   *   string value = 1 [(buf.validate.field).string.max_len = 10];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 max_len = 3;\n   */\n  maxLen: bigint;\n\n  /**\n   * `len_bytes` dictates that the field value must have the specified number of\n   * bytes. If the field value does not match the specified length in bytes,\n   * an error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value length must be 6 bytes\n   *   string value = 1 [(buf.validate.field).string.len_bytes = 6];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 len_bytes = 20;\n   */\n  lenBytes: bigint;\n\n  /**\n   * `min_bytes` specifies that the field value must have at least the specified\n   * number of bytes. If the field value contains fewer bytes, an error message\n   * will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value length must be at least 4 bytes\n   *   string value = 1 [(buf.validate.field).string.min_bytes = 4];\n   * }\n   *\n   * ```\n   *\n   * @generated from field: optional uint64 min_bytes = 4;\n   */\n  minBytes: bigint;\n\n  /**\n   * `max_bytes` specifies that the field value must have no more than the\n   * specified number of bytes. If the field value contains more bytes, an\n   * error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value length must be at most 8 bytes\n   *   string value = 1 [(buf.validate.field).string.max_bytes = 8];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 max_bytes = 5;\n   */\n  maxBytes: bigint;\n\n  /**\n   * `pattern` specifies that the field value must match the specified\n   * regular expression (RE2 syntax), with the expression provided without any\n   * delimiters. If the field value doesn't match the regular expression, an\n   * error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value does not match regex pattern `^[a-zA-Z]//$`\n   *   string value = 1 [(buf.validate.field).string.pattern = \"^[a-zA-Z]//$\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string pattern = 6;\n   */\n  pattern: string;\n\n  /**\n   * `prefix` specifies that the field value must have the\n   * specified substring at the beginning of the string. If the field value\n   * doesn't start with the specified prefix, an error message will be\n   * generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value does not have prefix `pre`\n   *   string value = 1 [(buf.validate.field).string.prefix = \"pre\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string prefix = 7;\n   */\n  prefix: string;\n\n  /**\n   * `suffix` specifies that the field value must have the\n   * specified substring at the end of the string. If the field value doesn't\n   * end with the specified suffix, an error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value does not have suffix `post`\n   *   string value = 1 [(buf.validate.field).string.suffix = \"post\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string suffix = 8;\n   */\n  suffix: string;\n\n  /**\n   * `contains` specifies that the field value must have the\n   * specified substring anywhere in the string. If the field value doesn't\n   * contain the specified substring, an error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value does not contain substring `inside`.\n   *   string value = 1 [(buf.validate.field).string.contains = \"inside\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string contains = 9;\n   */\n  contains: string;\n\n  /**\n   * `not_contains` specifies that the field value must not have the\n   * specified substring anywhere in the string. If the field value contains\n   * the specified substring, an error message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value contains substring `inside`.\n   *   string value = 1 [(buf.validate.field).string.not_contains = \"inside\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string not_contains = 23;\n   */\n  notContains: string;\n\n  /**\n   * `in` specifies that the field value must be equal to one of the specified\n   * values. If the field value isn't one of the specified values, an error\n   * message will be generated.\n   *\n   * ```proto\n   * message MyString {\n   *   // value must be in list [\"apple\", \"banana\"]\n   *   string value = 1 [(buf.validate.field).string.in = \"apple\", (buf.validate.field).string.in = \"banana\"];\n   * }\n   * ```\n   *\n   * @generated from field: repeated string in = 10;\n   */\n  in: string[];\n\n  /**\n   * `not_in` specifies that the field value cannot be equal to any\n   * of the specified values. If the field value is one of the specified values,\n   * an error message will be generated.\n   * ```proto\n   * message MyString {\n   *   // value must not be in list [\"orange\", \"grape\"]\n   *   string value = 1 [(buf.validate.field).string.not_in = \"orange\", (buf.validate.field).string.not_in = \"grape\"];\n   * }\n   * ```\n   *\n   * @generated from field: repeated string not_in = 11;\n   */\n  notIn: string[];\n\n  /**\n   * `WellKnown` rules provide advanced rules against common string\n   * patterns.\n   *\n   * @generated from oneof buf.validate.StringRules.well_known\n   */\n  wellKnown: {\n    /**\n     * `email` specifies that the field value must be a valid email address, for\n     * example \"foo@example.com\".\n     *\n     * Conforms to the definition for a valid email address from the [HTML standard](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address).\n     * Note that this standard willfully deviates from [RFC 5322](https://datatracker.ietf.org/doc/html/rfc5322),\n     * which allows many unexpected forms of email addresses and will easily match\n     * a typographical error.\n     *\n     * If the field value isn't a valid email address, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid email address\n     *   string value = 1 [(buf.validate.field).string.email = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool email = 12;\n     */\n    value: boolean;\n    case: \"email\";\n  } | {\n    /**\n     * `hostname` specifies that the field value must be a valid hostname, for\n     * example \"foo.example.com\".\n     *\n     * A valid hostname follows the rules below:\n     * - The name consists of one or more labels, separated by a dot (\".\").\n     * - Each label can be 1 to 63 alphanumeric characters.\n     * - A label can contain hyphens (\"-\"), but must not start or end with a hyphen.\n     * - The right-most label must not be digits only.\n     * - The name can have a trailing dot—for example, \"foo.example.com.\".\n     * - The name can be 253 characters at most, excluding the optional trailing dot.\n     *\n     * If the field value isn't a valid hostname, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid hostname\n     *   string value = 1 [(buf.validate.field).string.hostname = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool hostname = 13;\n     */\n    value: boolean;\n    case: \"hostname\";\n  } | {\n    /**\n     * `ip` specifies that the field value must be a valid IP (v4 or v6) address.\n     *\n     * IPv4 addresses are expected in the dotted decimal format—for example, \"192.168.5.21\".\n     * IPv6 addresses are expected in their text representation—for example, \"::1\",\n     * or \"2001:0DB8:ABCD:0012::0\".\n     *\n     * Both formats are well-defined in the internet standard [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986).\n     * Zone identifiers for IPv6 addresses (for example, \"fe80::a%en1\") are supported.\n     *\n     * If the field value isn't a valid IP address, an error message will be\n     * generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IP address\n     *   string value = 1 [(buf.validate.field).string.ip = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ip = 14;\n     */\n    value: boolean;\n    case: \"ip\";\n  } | {\n    /**\n     * `ipv4` specifies that the field value must be a valid IPv4 address—for\n     * example \"192.168.5.21\". If the field value isn't a valid IPv4 address, an\n     * error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IPv4 address\n     *   string value = 1 [(buf.validate.field).string.ipv4 = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv4 = 15;\n     */\n    value: boolean;\n    case: \"ipv4\";\n  } | {\n    /**\n     * `ipv6` specifies that the field value must be a valid IPv6 address—for\n     * example \"::1\", or \"d7a:115c:a1e0:ab12:4843:cd96:626b:430b\". If the field\n     * value is not a valid IPv6 address, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IPv6 address\n     *   string value = 1 [(buf.validate.field).string.ipv6 = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv6 = 16;\n     */\n    value: boolean;\n    case: \"ipv6\";\n  } | {\n    /**\n     * `uri` specifies that the field value must be a valid URI, for example\n     * \"https://example.com/foo/bar?baz=quux#frag\".\n     *\n     * URI is defined in the internet standard [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986).\n     * Zone Identifiers in IPv6 address literals are supported ([RFC 6874](https://datatracker.ietf.org/doc/html/rfc6874)).\n     *\n     * If the field value isn't a valid URI, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid URI\n     *   string value = 1 [(buf.validate.field).string.uri = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool uri = 17;\n     */\n    value: boolean;\n    case: \"uri\";\n  } | {\n    /**\n     * `uri_ref` specifies that the field value must be a valid URI Reference—either\n     * a URI such as \"https://example.com/foo/bar?baz=quux#frag\", or a Relative\n     * Reference such as \"./foo/bar?query\".\n     *\n     * URI, URI Reference, and Relative Reference are defined in the internet\n     * standard [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986). Zone\n     * Identifiers in IPv6 address literals are supported ([RFC 6874](https://datatracker.ietf.org/doc/html/rfc6874)).\n     *\n     * If the field value isn't a valid URI Reference, an error message will be\n     * generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid URI Reference\n     *   string value = 1 [(buf.validate.field).string.uri_ref = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool uri_ref = 18;\n     */\n    value: boolean;\n    case: \"uriRef\";\n  } | {\n    /**\n     * `address` specifies that the field value must be either a valid hostname\n     * (for example, \"example.com\"), or a valid IP (v4 or v6) address (for example,\n     * \"192.168.0.1\", or \"::1\"). If the field value isn't a valid hostname or IP,\n     * an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid hostname, or ip address\n     *   string value = 1 [(buf.validate.field).string.address = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool address = 21;\n     */\n    value: boolean;\n    case: \"address\";\n  } | {\n    /**\n     * `uuid` specifies that the field value must be a valid UUID as defined by\n     * [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.2). If the\n     * field value isn't a valid UUID, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid UUID\n     *   string value = 1 [(buf.validate.field).string.uuid = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool uuid = 22;\n     */\n    value: boolean;\n    case: \"uuid\";\n  } | {\n    /**\n     * `tuuid` (trimmed UUID) specifies that the field value must be a valid UUID as\n     * defined by [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.2) with all dashes\n     * omitted. If the field value isn't a valid UUID without dashes, an error message\n     * will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid trimmed UUID\n     *   string value = 1 [(buf.validate.field).string.tuuid = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool tuuid = 33;\n     */\n    value: boolean;\n    case: \"tuuid\";\n  } | {\n    /**\n     * `ip_with_prefixlen` specifies that the field value must be a valid IP\n     * (v4 or v6) address with prefix length—for example, \"192.168.5.21/16\" or\n     * \"2001:0DB8:ABCD:0012::F1/64\". If the field value isn't a valid IP with\n     * prefix length, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IP with prefix length\n     *    string value = 1 [(buf.validate.field).string.ip_with_prefixlen = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ip_with_prefixlen = 26;\n     */\n    value: boolean;\n    case: \"ipWithPrefixlen\";\n  } | {\n    /**\n     * `ipv4_with_prefixlen` specifies that the field value must be a valid\n     * IPv4 address with prefix length—for example, \"192.168.5.21/16\". If the\n     * field value isn't a valid IPv4 address with prefix length, an error\n     * message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IPv4 address with prefix length\n     *    string value = 1 [(buf.validate.field).string.ipv4_with_prefixlen = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv4_with_prefixlen = 27;\n     */\n    value: boolean;\n    case: \"ipv4WithPrefixlen\";\n  } | {\n    /**\n     * `ipv6_with_prefixlen` specifies that the field value must be a valid\n     * IPv6 address with prefix length—for example, \"2001:0DB8:ABCD:0012::F1/64\".\n     * If the field value is not a valid IPv6 address with prefix length,\n     * an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IPv6 address prefix length\n     *    string value = 1 [(buf.validate.field).string.ipv6_with_prefixlen = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv6_with_prefixlen = 28;\n     */\n    value: boolean;\n    case: \"ipv6WithPrefixlen\";\n  } | {\n    /**\n     * `ip_prefix` specifies that the field value must be a valid IP (v4 or v6)\n     * prefix—for example, \"192.168.0.0/16\" or \"2001:0DB8:ABCD:0012::0/64\".\n     *\n     * The prefix must have all zeros for the unmasked bits. For example,\n     * \"2001:0DB8:ABCD:0012::0/64\" designates the left-most 64 bits for the\n     * prefix, and the remaining 64 bits must be zero.\n     *\n     * If the field value isn't a valid IP prefix, an error message will be\n     * generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IP prefix\n     *    string value = 1 [(buf.validate.field).string.ip_prefix = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ip_prefix = 29;\n     */\n    value: boolean;\n    case: \"ipPrefix\";\n  } | {\n    /**\n     * `ipv4_prefix` specifies that the field value must be a valid IPv4\n     * prefix, for example \"192.168.0.0/16\".\n     *\n     * The prefix must have all zeros for the unmasked bits. For example,\n     * \"192.168.0.0/16\" designates the left-most 16 bits for the prefix,\n     * and the remaining 16 bits must be zero.\n     *\n     * If the field value isn't a valid IPv4 prefix, an error message\n     * will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IPv4 prefix\n     *    string value = 1 [(buf.validate.field).string.ipv4_prefix = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv4_prefix = 30;\n     */\n    value: boolean;\n    case: \"ipv4Prefix\";\n  } | {\n    /**\n     * `ipv6_prefix` specifies that the field value must be a valid IPv6 prefix—for\n     * example, \"2001:0DB8:ABCD:0012::0/64\".\n     *\n     * The prefix must have all zeros for the unmasked bits. For example,\n     * \"2001:0DB8:ABCD:0012::0/64\" designates the left-most 64 bits for the\n     * prefix, and the remaining 64 bits must be zero.\n     *\n     * If the field value is not a valid IPv6 prefix, an error message will be\n     * generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid IPv6 prefix\n     *    string value = 1 [(buf.validate.field).string.ipv6_prefix = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv6_prefix = 31;\n     */\n    value: boolean;\n    case: \"ipv6Prefix\";\n  } | {\n    /**\n     * `host_and_port` specifies that the field value must be valid host/port\n     * pair—for example, \"example.com:8080\".\n     *\n     * The host can be one of:\n     * - An IPv4 address in dotted decimal format—for example, \"192.168.5.21\".\n     * - An IPv6 address enclosed in square brackets—for example, \"[2001:0DB8:ABCD:0012::F1]\".\n     * - A hostname—for example, \"example.com\".\n     *\n     * The port is separated by a colon. It must be non-empty, with a decimal number\n     * in the range of 0-65535, inclusive.\n     *\n     * @generated from field: bool host_and_port = 32;\n     */\n    value: boolean;\n    case: \"hostAndPort\";\n  } | {\n    /**\n     * `well_known_regex` specifies a common well-known pattern\n     * defined as a regex. If the field value doesn't match the well-known\n     * regex, an error message will be generated.\n     *\n     * ```proto\n     * message MyString {\n     *   // value must be a valid HTTP header value\n     *   string value = 1 [(buf.validate.field).string.well_known_regex = KNOWN_REGEX_HTTP_HEADER_VALUE];\n     * }\n     * ```\n     *\n     * #### KnownRegex\n     *\n     * `well_known_regex` contains some well-known patterns.\n     *\n     * | Name                          | Number | Description                               |\n     * |-------------------------------|--------|-------------------------------------------|\n     * | KNOWN_REGEX_UNSPECIFIED       | 0      |                                           |\n     * | KNOWN_REGEX_HTTP_HEADER_NAME  | 1      | HTTP header name as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2)  |\n     * | KNOWN_REGEX_HTTP_HEADER_VALUE | 2      | HTTP header value as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4) |\n     *\n     * @generated from field: buf.validate.KnownRegex well_known_regex = 24;\n     */\n    value: KnownRegex;\n    case: \"wellKnownRegex\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * This applies to regexes `HTTP_HEADER_NAME` and `HTTP_HEADER_VALUE` to\n   * enable strict header validation. By default, this is true, and HTTP header\n   * validations are [RFC-compliant](https://datatracker.ietf.org/doc/html/rfc7230#section-3). Setting to false will enable looser\n   * validations that only disallow `\\r\\n\\0` characters, which can be used to\n   * bypass header matching rules.\n   *\n   * ```proto\n   * message MyString {\n   *   // The field `value` must have be a valid HTTP headers, but not enforced with strict rules.\n   *   string value = 1 [(buf.validate.field).string.strict = false];\n   * }\n   * ```\n   *\n   * @generated from field: optional bool strict = 25;\n   */\n  strict: boolean;\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyString {\n   *   string value = 1 [\n   *     (buf.validate.field).string.example = \"hello\",\n   *     (buf.validate.field).string.example = \"world\"\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated string example = 34;\n   */\n  example: string[];\n};\n\n/**\n * Describes the message buf.validate.StringRules.\n * Use `create(StringRulesSchema)` to create a new message.\n */\nexport const StringRulesSchema: GenMessage<StringRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 19);\n\n/**\n * BytesRules describe the rules applied to `bytes` values. These rules\n * may also be applied to the `google.protobuf.BytesValue` Well-Known-Type.\n *\n * @generated from message buf.validate.BytesRules\n */\nexport type BytesRules = Message<\"buf.validate.BytesRules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified bytes\n   * value. If the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value must be \"\\x01\\x02\\x03\\x04\"\n   *   bytes value = 1 [(buf.validate.field).bytes.const = \"\\x01\\x02\\x03\\x04\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional bytes const = 1;\n   */\n  const: Uint8Array;\n\n  /**\n   * `len` requires the field value to have the specified length in bytes.\n   * If the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value length must be 4 bytes.\n   *   optional bytes value = 1 [(buf.validate.field).bytes.len = 4];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 len = 13;\n   */\n  len: bigint;\n\n  /**\n   * `min_len` requires the field value to have at least the specified minimum\n   * length in bytes.\n   * If the field value doesn't meet the requirement, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value length must be at least 2 bytes.\n   *   optional bytes value = 1 [(buf.validate.field).bytes.min_len = 2];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 min_len = 2;\n   */\n  minLen: bigint;\n\n  /**\n   * `max_len` requires the field value to have at most the specified maximum\n   * length in bytes.\n   * If the field value exceeds the requirement, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value must be at most 6 bytes.\n   *   optional bytes value = 1 [(buf.validate.field).bytes.max_len = 6];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 max_len = 3;\n   */\n  maxLen: bigint;\n\n  /**\n   * `pattern` requires the field value to match the specified regular\n   * expression ([RE2 syntax](https://github.com/google/re2/wiki/Syntax)).\n   * The value of the field must be valid UTF-8 or validation will fail with a\n   * runtime error.\n   * If the field value doesn't match the pattern, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value must match regex pattern \"^[a-zA-Z0-9]+$\".\n   *   optional bytes value = 1 [(buf.validate.field).bytes.pattern = \"^[a-zA-Z0-9]+$\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional string pattern = 4;\n   */\n  pattern: string;\n\n  /**\n   * `prefix` requires the field value to have the specified bytes at the\n   * beginning of the string.\n   * If the field value doesn't meet the requirement, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value does not have prefix \\x01\\x02\n   *   optional bytes value = 1 [(buf.validate.field).bytes.prefix = \"\\x01\\x02\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional bytes prefix = 5;\n   */\n  prefix: Uint8Array;\n\n  /**\n   * `suffix` requires the field value to have the specified bytes at the end\n   * of the string.\n   * If the field value doesn't meet the requirement, an error message is generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value does not have suffix \\x03\\x04\n   *   optional bytes value = 1 [(buf.validate.field).bytes.suffix = \"\\x03\\x04\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional bytes suffix = 6;\n   */\n  suffix: Uint8Array;\n\n  /**\n   * `contains` requires the field value to have the specified bytes anywhere in\n   * the string.\n   * If the field value doesn't meet the requirement, an error message is generated.\n   *\n   * ```protobuf\n   * message MyBytes {\n   *   // value does not contain \\x02\\x03\n   *   optional bytes value = 1 [(buf.validate.field).bytes.contains = \"\\x02\\x03\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional bytes contains = 7;\n   */\n  contains: Uint8Array;\n\n  /**\n   * `in` requires the field value to be equal to one of the specified\n   * values. If the field value doesn't match any of the specified values, an\n   * error message is generated.\n   *\n   * ```protobuf\n   * message MyBytes {\n   *   // value must in [\"\\x01\\x02\", \"\\x02\\x03\", \"\\x03\\x04\"]\n   *   optional bytes value = 1 [(buf.validate.field).bytes.in = {\"\\x01\\x02\", \"\\x02\\x03\", \"\\x03\\x04\"}];\n   * }\n   * ```\n   *\n   * @generated from field: repeated bytes in = 8;\n   */\n  in: Uint8Array[];\n\n  /**\n   * `not_in` requires the field value to be not equal to any of the specified\n   * values.\n   * If the field value matches any of the specified values, an error message is\n   * generated.\n   *\n   * ```proto\n   * message MyBytes {\n   *   // value must not in [\"\\x01\\x02\", \"\\x02\\x03\", \"\\x03\\x04\"]\n   *   optional bytes value = 1 [(buf.validate.field).bytes.not_in = {\"\\x01\\x02\", \"\\x02\\x03\", \"\\x03\\x04\"}];\n   * }\n   * ```\n   *\n   * @generated from field: repeated bytes not_in = 9;\n   */\n  notIn: Uint8Array[];\n\n  /**\n   * WellKnown rules provide advanced rules against common byte\n   * patterns\n   *\n   * @generated from oneof buf.validate.BytesRules.well_known\n   */\n  wellKnown: {\n    /**\n     * `ip` ensures that the field `value` is a valid IP address (v4 or v6) in byte format.\n     * If the field value doesn't meet this rule, an error message is generated.\n     *\n     * ```proto\n     * message MyBytes {\n     *   // value must be a valid IP address\n     *   optional bytes value = 1 [(buf.validate.field).bytes.ip = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ip = 10;\n     */\n    value: boolean;\n    case: \"ip\";\n  } | {\n    /**\n     * `ipv4` ensures that the field `value` is a valid IPv4 address in byte format.\n     * If the field value doesn't meet this rule, an error message is generated.\n     *\n     * ```proto\n     * message MyBytes {\n     *   // value must be a valid IPv4 address\n     *   optional bytes value = 1 [(buf.validate.field).bytes.ipv4 = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv4 = 11;\n     */\n    value: boolean;\n    case: \"ipv4\";\n  } | {\n    /**\n     * `ipv6` ensures that the field `value` is a valid IPv6 address in byte format.\n     * If the field value doesn't meet this rule, an error message is generated.\n     * ```proto\n     * message MyBytes {\n     *   // value must be a valid IPv6 address\n     *   optional bytes value = 1 [(buf.validate.field).bytes.ipv6 = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool ipv6 = 12;\n     */\n    value: boolean;\n    case: \"ipv6\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyBytes {\n   *   bytes value = 1 [\n   *     (buf.validate.field).bytes.example = \"\\x01\\x02\",\n   *     (buf.validate.field).bytes.example = \"\\x02\\x03\"\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated bytes example = 14;\n   */\n  example: Uint8Array[];\n};\n\n/**\n * Describes the message buf.validate.BytesRules.\n * Use `create(BytesRulesSchema)` to create a new message.\n */\nexport const BytesRulesSchema: GenMessage<BytesRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 20);\n\n/**\n * EnumRules describe the rules applied to `enum` values.\n *\n * @generated from message buf.validate.EnumRules\n */\nexport type EnumRules = Message<\"buf.validate.EnumRules\"> & {\n  /**\n   * `const` requires the field value to exactly match the specified enum value.\n   * If the field value doesn't match, an error message is generated.\n   *\n   * ```proto\n   * enum MyEnum {\n   *   MY_ENUM_UNSPECIFIED = 0;\n   *   MY_ENUM_VALUE1 = 1;\n   *   MY_ENUM_VALUE2 = 2;\n   * }\n   *\n   * message MyMessage {\n   *   // The field `value` must be exactly MY_ENUM_VALUE1.\n   *   MyEnum value = 1 [(buf.validate.field).enum.const = 1];\n   * }\n   * ```\n   *\n   * @generated from field: optional int32 const = 1;\n   */\n  const: number;\n\n  /**\n   * `defined_only` requires the field value to be one of the defined values for\n   * this enum, failing on any undefined value.\n   *\n   * ```proto\n   * enum MyEnum {\n   *   MY_ENUM_UNSPECIFIED = 0;\n   *   MY_ENUM_VALUE1 = 1;\n   *   MY_ENUM_VALUE2 = 2;\n   * }\n   *\n   * message MyMessage {\n   *   // The field `value` must be a defined value of MyEnum.\n   *   MyEnum value = 1 [(buf.validate.field).enum.defined_only = true];\n   * }\n   * ```\n   *\n   * @generated from field: optional bool defined_only = 2;\n   */\n  definedOnly: boolean;\n\n  /**\n   * `in` requires the field value to be equal to one of the\n   * specified enum values. If the field value doesn't match any of the\n   * specified values, an error message is generated.\n   *\n   * ```proto\n   * enum MyEnum {\n   *   MY_ENUM_UNSPECIFIED = 0;\n   *   MY_ENUM_VALUE1 = 1;\n   *   MY_ENUM_VALUE2 = 2;\n   * }\n   *\n   * message MyMessage {\n   *   // The field `value` must be equal to one of the specified values.\n   *   MyEnum value = 1 [(buf.validate.field).enum = { in: [1, 2]}];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int32 in = 3;\n   */\n  in: number[];\n\n  /**\n   * `not_in` requires the field value to be not equal to any of the\n   * specified enum values. If the field value matches one of the specified\n   * values, an error message is generated.\n   *\n   * ```proto\n   * enum MyEnum {\n   *   MY_ENUM_UNSPECIFIED = 0;\n   *   MY_ENUM_VALUE1 = 1;\n   *   MY_ENUM_VALUE2 = 2;\n   * }\n   *\n   * message MyMessage {\n   *   // The field `value` must not be equal to any of the specified values.\n   *   MyEnum value = 1 [(buf.validate.field).enum = { not_in: [1, 2]}];\n   * }\n   * ```\n   *\n   * @generated from field: repeated int32 not_in = 4;\n   */\n  notIn: number[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * enum MyEnum {\n   *   MY_ENUM_UNSPECIFIED = 0;\n   *   MY_ENUM_VALUE1 = 1;\n   *   MY_ENUM_VALUE2 = 2;\n   * }\n   *\n   * message MyMessage {\n   *     (buf.validate.field).enum.example = 1,\n   *     (buf.validate.field).enum.example = 2\n   * }\n   * ```\n   *\n   * @generated from field: repeated int32 example = 5;\n   */\n  example: number[];\n};\n\n/**\n * Describes the message buf.validate.EnumRules.\n * Use `create(EnumRulesSchema)` to create a new message.\n */\nexport const EnumRulesSchema: GenMessage<EnumRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 21);\n\n/**\n * RepeatedRules describe the rules applied to `repeated` values.\n *\n * @generated from message buf.validate.RepeatedRules\n */\nexport type RepeatedRules = Message<\"buf.validate.RepeatedRules\"> & {\n  /**\n   * `min_items` requires that this field must contain at least the specified\n   * minimum number of items.\n   *\n   * Note that `min_items = 1` is equivalent to setting a field as `required`.\n   *\n   * ```proto\n   * message MyRepeated {\n   *   // value must contain at least  2 items\n   *   repeated string value = 1 [(buf.validate.field).repeated.min_items = 2];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 min_items = 1;\n   */\n  minItems: bigint;\n\n  /**\n   * `max_items` denotes that this field must not exceed a\n   * certain number of items as the upper limit. If the field contains more\n   * items than specified, an error message will be generated, requiring the\n   * field to maintain no more than the specified number of items.\n   *\n   * ```proto\n   * message MyRepeated {\n   *   // value must contain no more than 3 item(s)\n   *   repeated string value = 1 [(buf.validate.field).repeated.max_items = 3];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 max_items = 2;\n   */\n  maxItems: bigint;\n\n  /**\n   * `unique` indicates that all elements in this field must\n   * be unique. This rule is strictly applicable to scalar and enum\n   * types, with message types not being supported.\n   *\n   * ```proto\n   * message MyRepeated {\n   *   // repeated value must contain unique items\n   *   repeated string value = 1 [(buf.validate.field).repeated.unique = true];\n   * }\n   * ```\n   *\n   * @generated from field: optional bool unique = 3;\n   */\n  unique: boolean;\n\n  /**\n   * `items` details the rules to be applied to each item\n   * in the field. Even for repeated message fields, validation is executed\n   * against each item unless `ignore` is specified.\n   *\n   * ```proto\n   * message MyRepeated {\n   *   // The items in the field `value` must follow the specified rules.\n   *   repeated string value = 1 [(buf.validate.field).repeated.items = {\n   *     string: {\n   *       min_len: 3\n   *       max_len: 10\n   *     }\n   *   }];\n   * }\n   * ```\n   *\n   * Note that the `required` rule does not apply. Repeated items\n   * cannot be unset.\n   *\n   * @generated from field: optional buf.validate.FieldRules items = 4;\n   */\n  items?: FieldRules;\n};\n\n/**\n * Describes the message buf.validate.RepeatedRules.\n * Use `create(RepeatedRulesSchema)` to create a new message.\n */\nexport const RepeatedRulesSchema: GenMessage<RepeatedRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 22);\n\n/**\n * MapRules describe the rules applied to `map` values.\n *\n * @generated from message buf.validate.MapRules\n */\nexport type MapRules = Message<\"buf.validate.MapRules\"> & {\n  /**\n   * Specifies the minimum number of key-value pairs allowed. If the field has\n   * fewer key-value pairs than specified, an error message is generated.\n   *\n   * ```proto\n   * message MyMap {\n   *   // The field `value` must have at least 2 key-value pairs.\n   *   map<string, string> value = 1 [(buf.validate.field).map.min_pairs = 2];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 min_pairs = 1;\n   */\n  minPairs: bigint;\n\n  /**\n   * Specifies the maximum number of key-value pairs allowed. If the field has\n   * more key-value pairs than specified, an error message is generated.\n   *\n   * ```proto\n   * message MyMap {\n   *   // The field `value` must have at most 3 key-value pairs.\n   *   map<string, string> value = 1 [(buf.validate.field).map.max_pairs = 3];\n   * }\n   * ```\n   *\n   * @generated from field: optional uint64 max_pairs = 2;\n   */\n  maxPairs: bigint;\n\n  /**\n   * Specifies the rules to be applied to each key in the field.\n   *\n   * ```proto\n   * message MyMap {\n   *   // The keys in the field `value` must follow the specified rules.\n   *   map<string, string> value = 1 [(buf.validate.field).map.keys = {\n   *     string: {\n   *       min_len: 3\n   *       max_len: 10\n   *     }\n   *   }];\n   * }\n   * ```\n   *\n   * Note that the `required` rule does not apply. Map keys cannot be unset.\n   *\n   * @generated from field: optional buf.validate.FieldRules keys = 4;\n   */\n  keys?: FieldRules;\n\n  /**\n   * Specifies the rules to be applied to the value of each key in the\n   * field. Message values will still have their validations evaluated unless\n   * `ignore` is specified.\n   *\n   * ```proto\n   * message MyMap {\n   *   // The values in the field `value` must follow the specified rules.\n   *   map<string, string> value = 1 [(buf.validate.field).map.values = {\n   *     string: {\n   *       min_len: 5\n   *       max_len: 20\n   *     }\n   *   }];\n   * }\n   * ```\n   * Note that the `required` rule does not apply. Map values cannot be unset.\n   *\n   * @generated from field: optional buf.validate.FieldRules values = 5;\n   */\n  values?: FieldRules;\n};\n\n/**\n * Describes the message buf.validate.MapRules.\n * Use `create(MapRulesSchema)` to create a new message.\n */\nexport const MapRulesSchema: GenMessage<MapRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 23);\n\n/**\n * AnyRules describe rules applied exclusively to the `google.protobuf.Any` well-known type.\n *\n * @generated from message buf.validate.AnyRules\n */\nexport type AnyRules = Message<\"buf.validate.AnyRules\"> & {\n  /**\n   * `in` requires the field's `type_url` to be equal to one of the\n   * specified values. If it doesn't match any of the specified values, an error\n   * message is generated.\n   *\n   * ```proto\n   * message MyAny {\n   *   //  The `value` field must have a `type_url` equal to one of the specified values.\n   *   google.protobuf.Any value = 1 [(buf.validate.field).any = {\n   *       in: [\"type.googleapis.com/MyType1\", \"type.googleapis.com/MyType2\"]\n   *   }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated string in = 2;\n   */\n  in: string[];\n\n  /**\n   * requires the field's type_url to be not equal to any of the specified values. If it matches any of the specified values, an error message is generated.\n   *\n   * ```proto\n   * message MyAny {\n   *   //  The `value` field must not have a `type_url` equal to any of the specified values.\n   *   google.protobuf.Any value = 1 [(buf.validate.field).any = {\n   *       not_in: [\"type.googleapis.com/ForbiddenType1\", \"type.googleapis.com/ForbiddenType2\"]\n   *   }];\n   * }\n   * ```\n   *\n   * @generated from field: repeated string not_in = 3;\n   */\n  notIn: string[];\n};\n\n/**\n * Describes the message buf.validate.AnyRules.\n * Use `create(AnyRulesSchema)` to create a new message.\n */\nexport const AnyRulesSchema: GenMessage<AnyRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 24);\n\n/**\n * DurationRules describe the rules applied exclusively to the `google.protobuf.Duration` well-known type.\n *\n * @generated from message buf.validate.DurationRules\n */\nexport type DurationRules = Message<\"buf.validate.DurationRules\"> & {\n  /**\n   * `const` dictates that the field must match the specified value of the `google.protobuf.Duration` type exactly.\n   * If the field's value deviates from the specified value, an error message\n   * will be generated.\n   *\n   * ```proto\n   * message MyDuration {\n   *   // value must equal 5s\n   *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.const = \"5s\"];\n   * }\n   * ```\n   *\n   * @generated from field: optional google.protobuf.Duration const = 2;\n   */\n  const?: Duration;\n\n  /**\n   * @generated from oneof buf.validate.DurationRules.less_than\n   */\n  lessThan: {\n    /**\n     * `lt` stipulates that the field must be less than the specified value of the `google.protobuf.Duration` type,\n     * exclusive. If the field's value is greater than or equal to the specified\n     * value, an error message will be generated.\n     *\n     * ```proto\n     * message MyDuration {\n     *   // value must be less than 5s\n     *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.lt = \"5s\"];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Duration lt = 3;\n     */\n    value: Duration;\n    case: \"lt\";\n  } | {\n    /**\n     * `lte` indicates that the field must be less than or equal to the specified\n     * value of the `google.protobuf.Duration` type, inclusive. If the field's value is greater than the specified value,\n     * an error message will be generated.\n     *\n     * ```proto\n     * message MyDuration {\n     *   // value must be less than or equal to 10s\n     *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.lte = \"10s\"];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Duration lte = 4;\n     */\n    value: Duration;\n    case: \"lte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.DurationRules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the duration field value to be greater than the specified\n     * value (exclusive). If the value of `gt` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyDuration {\n     *   // duration must be greater than 5s [duration.gt]\n     *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.gt = { seconds: 5 }];\n     *\n     *   // duration must be greater than 5s and less than 10s [duration.gt_lt]\n     *   google.protobuf.Duration another_value = 2 [(buf.validate.field).duration = { gt: { seconds: 5 }, lt: { seconds: 10 } }];\n     *\n     *   // duration must be greater than 10s or less than 5s [duration.gt_lt_exclusive]\n     *   google.protobuf.Duration other_value = 3 [(buf.validate.field).duration = { gt: { seconds: 10 }, lt: { seconds: 5 } }];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Duration gt = 5;\n     */\n    value: Duration;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the duration field value to be greater than or equal to the\n     * specified value (exclusive). If the value of `gte` is larger than a\n     * specified `lt` or `lte`, the range is reversed, and the field value must\n     * be outside the specified range. If the field value doesn't meet the\n     * required conditions, an error message is generated.\n     *\n     * ```proto\n     * message MyDuration {\n     *  // duration must be greater than or equal to 5s [duration.gte]\n     *  google.protobuf.Duration value = 1 [(buf.validate.field).duration.gte = { seconds: 5 }];\n     *\n     *  // duration must be greater than or equal to 5s and less than 10s [duration.gte_lt]\n     *  google.protobuf.Duration another_value = 2 [(buf.validate.field).duration = { gte: { seconds: 5 }, lt: { seconds: 10 } }];\n     *\n     *  // duration must be greater than or equal to 10s or less than 5s [duration.gte_lt_exclusive]\n     *  google.protobuf.Duration other_value = 3 [(buf.validate.field).duration = { gte: { seconds: 10 }, lt: { seconds: 5 } }];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Duration gte = 6;\n     */\n    value: Duration;\n    case: \"gte\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `in` asserts that the field must be equal to one of the specified values of the `google.protobuf.Duration` type.\n   * If the field's value doesn't correspond to any of the specified values,\n   * an error message will be generated.\n   *\n   * ```proto\n   * message MyDuration {\n   *   // value must be in list [1s, 2s, 3s]\n   *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.in = [\"1s\", \"2s\", \"3s\"]];\n   * }\n   * ```\n   *\n   * @generated from field: repeated google.protobuf.Duration in = 7;\n   */\n  in: Duration[];\n\n  /**\n   * `not_in` denotes that the field must not be equal to\n   * any of the specified values of the `google.protobuf.Duration` type.\n   * If the field's value matches any of these values, an error message will be\n   * generated.\n   *\n   * ```proto\n   * message MyDuration {\n   *   // value must not be in list [1s, 2s, 3s]\n   *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.not_in = [\"1s\", \"2s\", \"3s\"]];\n   * }\n   * ```\n   *\n   * @generated from field: repeated google.protobuf.Duration not_in = 8;\n   */\n  notIn: Duration[];\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyDuration {\n   *   google.protobuf.Duration value = 1 [\n   *     (buf.validate.field).duration.example = { seconds: 1 },\n   *     (buf.validate.field).duration.example = { seconds: 2 },\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated google.protobuf.Duration example = 9;\n   */\n  example: Duration[];\n};\n\n/**\n * Describes the message buf.validate.DurationRules.\n * Use `create(DurationRulesSchema)` to create a new message.\n */\nexport const DurationRulesSchema: GenMessage<DurationRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 25);\n\n/**\n * TimestampRules describe the rules applied exclusively to the `google.protobuf.Timestamp` well-known type.\n *\n * @generated from message buf.validate.TimestampRules\n */\nexport type TimestampRules = Message<\"buf.validate.TimestampRules\"> & {\n  /**\n   * `const` dictates that this field, of the `google.protobuf.Timestamp` type, must exactly match the specified value. If the field value doesn't correspond to the specified timestamp, an error message will be generated.\n   *\n   * ```proto\n   * message MyTimestamp {\n   *   // value must equal 2023-05-03T10:00:00Z\n   *   google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.const = {seconds: 1727998800}];\n   * }\n   * ```\n   *\n   * @generated from field: optional google.protobuf.Timestamp const = 2;\n   */\n  const?: Timestamp;\n\n  /**\n   * @generated from oneof buf.validate.TimestampRules.less_than\n   */\n  lessThan: {\n    /**\n     * requires the duration field value to be less than the specified value (field < value). If the field value doesn't meet the required conditions, an error message is generated.\n     *\n     * ```proto\n     * message MyDuration {\n     *   // duration must be less than 'P3D' [duration.lt]\n     *   google.protobuf.Duration value = 1 [(buf.validate.field).duration.lt = { seconds: 259200 }];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Timestamp lt = 3;\n     */\n    value: Timestamp;\n    case: \"lt\";\n  } | {\n    /**\n     * requires the timestamp field value to be less than or equal to the specified value (field <= value). If the field value doesn't meet the required conditions, an error message is generated.\n     *\n     * ```proto\n     * message MyTimestamp {\n     *   // timestamp must be less than or equal to '2023-05-14T00:00:00Z' [timestamp.lte]\n     *   google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.lte = { seconds: 1678867200 }];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Timestamp lte = 4;\n     */\n    value: Timestamp;\n    case: \"lte\";\n  } | {\n    /**\n     * `lt_now` specifies that this field, of the `google.protobuf.Timestamp` type, must be less than the current time. `lt_now` can only be used with the `within` rule.\n     *\n     * ```proto\n     * message MyTimestamp {\n     *  // value must be less than now\n     *   google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.lt_now = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool lt_now = 7;\n     */\n    value: boolean;\n    case: \"ltNow\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * @generated from oneof buf.validate.TimestampRules.greater_than\n   */\n  greaterThan: {\n    /**\n     * `gt` requires the timestamp field value to be greater than the specified\n     * value (exclusive). If the value of `gt` is larger than a specified `lt`\n     * or `lte`, the range is reversed, and the field value must be outside the\n     * specified range. If the field value doesn't meet the required conditions,\n     * an error message is generated.\n     *\n     * ```proto\n     * message MyTimestamp {\n     *   // timestamp must be greater than '2023-01-01T00:00:00Z' [timestamp.gt]\n     *   google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.gt = { seconds: 1672444800 }];\n     *\n     *   // timestamp must be greater than '2023-01-01T00:00:00Z' and less than '2023-01-02T00:00:00Z' [timestamp.gt_lt]\n     *   google.protobuf.Timestamp another_value = 2 [(buf.validate.field).timestamp = { gt: { seconds: 1672444800 }, lt: { seconds: 1672531200 } }];\n     *\n     *   // timestamp must be greater than '2023-01-02T00:00:00Z' or less than '2023-01-01T00:00:00Z' [timestamp.gt_lt_exclusive]\n     *   google.protobuf.Timestamp other_value = 3 [(buf.validate.field).timestamp = { gt: { seconds: 1672531200 }, lt: { seconds: 1672444800 } }];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Timestamp gt = 5;\n     */\n    value: Timestamp;\n    case: \"gt\";\n  } | {\n    /**\n     * `gte` requires the timestamp field value to be greater than or equal to the\n     * specified value (exclusive). If the value of `gte` is larger than a\n     * specified `lt` or `lte`, the range is reversed, and the field value\n     * must be outside the specified range. If the field value doesn't meet\n     * the required conditions, an error message is generated.\n     *\n     * ```proto\n     * message MyTimestamp {\n     *   // timestamp must be greater than or equal to '2023-01-01T00:00:00Z' [timestamp.gte]\n     *   google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.gte = { seconds: 1672444800 }];\n     *\n     *   // timestamp must be greater than or equal to '2023-01-01T00:00:00Z' and less than '2023-01-02T00:00:00Z' [timestamp.gte_lt]\n     *   google.protobuf.Timestamp another_value = 2 [(buf.validate.field).timestamp = { gte: { seconds: 1672444800 }, lt: { seconds: 1672531200 } }];\n     *\n     *   // timestamp must be greater than or equal to '2023-01-02T00:00:00Z' or less than '2023-01-01T00:00:00Z' [timestamp.gte_lt_exclusive]\n     *   google.protobuf.Timestamp other_value = 3 [(buf.validate.field).timestamp = { gte: { seconds: 1672531200 }, lt: { seconds: 1672444800 } }];\n     * }\n     * ```\n     *\n     * @generated from field: google.protobuf.Timestamp gte = 6;\n     */\n    value: Timestamp;\n    case: \"gte\";\n  } | {\n    /**\n     * `gt_now` specifies that this field, of the `google.protobuf.Timestamp` type, must be greater than the current time. `gt_now` can only be used with the `within` rule.\n     *\n     * ```proto\n     * message MyTimestamp {\n     *   // value must be greater than now\n     *   google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.gt_now = true];\n     * }\n     * ```\n     *\n     * @generated from field: bool gt_now = 8;\n     */\n    value: boolean;\n    case: \"gtNow\";\n  } | { case: undefined; value?: undefined };\n\n  /**\n   * `within` specifies that this field, of the `google.protobuf.Timestamp` type, must be within the specified duration of the current time. If the field value isn't within the duration, an error message is generated.\n   *\n   * ```proto\n   * message MyTimestamp {\n   *   // value must be within 1 hour of now\n   *   google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.within = {seconds: 3600}];\n   * }\n   * ```\n   *\n   * @generated from field: optional google.protobuf.Duration within = 9;\n   */\n  within?: Duration;\n\n  /**\n   * `example` specifies values that the field may have. These values SHOULD\n   * conform to other rules. `example` values will not impact validation\n   * but may be used as helpful guidance on how to populate the given field.\n   *\n   * ```proto\n   * message MyTimestamp {\n   *   google.protobuf.Timestamp value = 1 [\n   *     (buf.validate.field).timestamp.example = { seconds: 1672444800 },\n   *     (buf.validate.field).timestamp.example = { seconds: 1672531200 },\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from field: repeated google.protobuf.Timestamp example = 10;\n   */\n  example: Timestamp[];\n};\n\n/**\n * Describes the message buf.validate.TimestampRules.\n * Use `create(TimestampRulesSchema)` to create a new message.\n */\nexport const TimestampRulesSchema: GenMessage<TimestampRules> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 26);\n\n/**\n * `Violations` is a collection of `Violation` messages. This message type is returned by\n * Protovalidate when a proto message fails to meet the requirements set by the `Rule` validation rules.\n * Each individual violation is represented by a `Violation` message.\n *\n * @generated from message buf.validate.Violations\n */\nexport type Violations = Message<\"buf.validate.Violations\"> & {\n  /**\n   * `violations` is a repeated field that contains all the `Violation` messages corresponding to the violations detected.\n   *\n   * @generated from field: repeated buf.validate.Violation violations = 1;\n   */\n  violations: Violation[];\n};\n\n/**\n * Describes the message buf.validate.Violations.\n * Use `create(ViolationsSchema)` to create a new message.\n */\nexport const ViolationsSchema: GenMessage<Violations> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 27);\n\n/**\n * `Violation` represents a single instance where a validation rule, expressed\n * as a `Rule`, was not met. It provides information about the field that\n * caused the violation, the specific rule that wasn't fulfilled, and a\n * human-readable error message.\n *\n * For example, consider the following message:\n *\n * ```proto\n * message User {\n *     int32 age = 1 [(buf.validate.field).cel = {\n *         id: \"user.age\",\n *         expression: \"this < 18 ? 'User must be at least 18 years old' : ''\",\n *     }];\n * }\n * ```\n *\n * It could produce the following violation:\n *\n * ```json\n * {\n *   \"ruleId\": \"user.age\",\n *   \"message\": \"User must be at least 18 years old\",\n *   \"field\": {\n *     \"elements\": [\n *       {\n *         \"fieldNumber\": 1,\n *         \"fieldName\": \"age\",\n *         \"fieldType\": \"TYPE_INT32\"\n *       }\n *     ]\n *   },\n *   \"rule\": {\n *     \"elements\": [\n *       {\n *         \"fieldNumber\": 23,\n *         \"fieldName\": \"cel\",\n *         \"fieldType\": \"TYPE_MESSAGE\",\n *         \"index\": \"0\"\n *       }\n *     ]\n *   }\n * }\n * ```\n *\n * @generated from message buf.validate.Violation\n */\nexport type Violation = Message<\"buf.validate.Violation\"> & {\n  /**\n   * `field` is a machine-readable path to the field that failed validation.\n   * This could be a nested field, in which case the path will include all the parent fields leading to the actual field that caused the violation.\n   *\n   * For example, consider the following message:\n   *\n   * ```proto\n   * message Message {\n   *   bool a = 1 [(buf.validate.field).required = true];\n   * }\n   * ```\n   *\n   * It could produce the following violation:\n   *\n   * ```textproto\n   * violation {\n   *   field { element { field_number: 1, field_name: \"a\", field_type: 8 } }\n   *   ...\n   * }\n   * ```\n   *\n   * @generated from field: optional buf.validate.FieldPath field = 5;\n   */\n  field?: FieldPath;\n\n  /**\n   * `rule` is a machine-readable path that points to the specific rule that failed validation.\n   * This will be a nested field starting from the FieldRules of the field that failed validation.\n   * For custom rules, this will provide the path of the rule, e.g. `cel[0]`.\n   *\n   * For example, consider the following message:\n   *\n   * ```proto\n   * message Message {\n   *   bool a = 1 [(buf.validate.field).required = true];\n   *   bool b = 2 [(buf.validate.field).cel = {\n   *     id: \"custom_rule\",\n   *     expression: \"!this ? 'b must be true': ''\"\n   *   }]\n   * }\n   * ```\n   *\n   * It could produce the following violations:\n   *\n   * ```textproto\n   * violation {\n   *   rule { element { field_number: 25, field_name: \"required\", field_type: 8 } }\n   *   ...\n   * }\n   * violation {\n   *   rule { element { field_number: 23, field_name: \"cel\", field_type: 11, index: 0 } }\n   *   ...\n   * }\n   * ```\n   *\n   * @generated from field: optional buf.validate.FieldPath rule = 6;\n   */\n  rule?: FieldPath;\n\n  /**\n   * `rule_id` is the unique identifier of the `Rule` that was not fulfilled.\n   * This is the same `id` that was specified in the `Rule` message, allowing easy tracing of which rule was violated.\n   *\n   * @generated from field: optional string rule_id = 2;\n   */\n  ruleId: string;\n\n  /**\n   * `message` is a human-readable error message that describes the nature of the violation.\n   * This can be the default error message from the violated `Rule`, or it can be a custom message that gives more context about the violation.\n   *\n   * @generated from field: optional string message = 3;\n   */\n  message: string;\n\n  /**\n   * `for_key` indicates whether the violation was caused by a map key, rather than a value.\n   *\n   * @generated from field: optional bool for_key = 4;\n   */\n  forKey: boolean;\n};\n\n/**\n * Describes the message buf.validate.Violation.\n * Use `create(ViolationSchema)` to create a new message.\n */\nexport const ViolationSchema: GenMessage<Violation> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 28);\n\n/**\n * `FieldPath` provides a path to a nested protobuf field.\n *\n * This message provides enough information to render a dotted field path even without protobuf descriptors.\n * It also provides enough information to resolve a nested field through unknown wire data.\n *\n * @generated from message buf.validate.FieldPath\n */\nexport type FieldPath = Message<\"buf.validate.FieldPath\"> & {\n  /**\n   * `elements` contains each element of the path, starting from the root and recursing downward.\n   *\n   * @generated from field: repeated buf.validate.FieldPathElement elements = 1;\n   */\n  elements: FieldPathElement[];\n};\n\n/**\n * Describes the message buf.validate.FieldPath.\n * Use `create(FieldPathSchema)` to create a new message.\n */\nexport const FieldPathSchema: GenMessage<FieldPath> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 29);\n\n/**\n * `FieldPathElement` provides enough information to nest through a single protobuf field.\n *\n * If the selected field is a map or repeated field, the `subscript` value selects a specific element from it.\n * A path that refers to a value nested under a map key or repeated field index will have a `subscript` value.\n * The `field_type` field allows unambiguous resolution of a field even if descriptors are not available.\n *\n * @generated from message buf.validate.FieldPathElement\n */\nexport type FieldPathElement = Message<\"buf.validate.FieldPathElement\"> & {\n  /**\n   * `field_number` is the field number this path element refers to.\n   *\n   * @generated from field: optional int32 field_number = 1;\n   */\n  fieldNumber: number;\n\n  /**\n   * `field_name` contains the field name this path element refers to.\n   * This can be used to display a human-readable path even if the field number is unknown.\n   *\n   * @generated from field: optional string field_name = 2;\n   */\n  fieldName: string;\n\n  /**\n   * `field_type` specifies the type of this field. When using reflection, this value is not needed.\n   *\n   * This value is provided to make it possible to traverse unknown fields through wire data.\n   * When traversing wire data, be mindful of both packed[1] and delimited[2] encoding schemes.\n   *\n   * [1]: https://protobuf.dev/programming-guides/encoding/#packed\n   * [2]: https://protobuf.dev/programming-guides/encoding/#groups\n   *\n   * N.B.: Although groups are deprecated, the corresponding delimited encoding scheme is not, and\n   * can be explicitly used in Protocol Buffers 2023 Edition.\n   *\n   * @generated from field: optional google.protobuf.FieldDescriptorProto.Type field_type = 3;\n   */\n  fieldType: FieldDescriptorProto_Type;\n\n  /**\n   * `key_type` specifies the map key type of this field. This value is useful when traversing\n   * unknown fields through wire data: specifically, it allows handling the differences between\n   * different integer encodings.\n   *\n   * @generated from field: optional google.protobuf.FieldDescriptorProto.Type key_type = 4;\n   */\n  keyType: FieldDescriptorProto_Type;\n\n  /**\n   * `value_type` specifies map value type of this field. This is useful if you want to display a\n   * value inside unknown fields through wire data.\n   *\n   * @generated from field: optional google.protobuf.FieldDescriptorProto.Type value_type = 5;\n   */\n  valueType: FieldDescriptorProto_Type;\n\n  /**\n   * `subscript` contains a repeated index or map key, if this path element nests into a repeated or map field.\n   *\n   * @generated from oneof buf.validate.FieldPathElement.subscript\n   */\n  subscript: {\n    /**\n     * `index` specifies a 0-based index into a repeated field.\n     *\n     * @generated from field: uint64 index = 6;\n     */\n    value: bigint;\n    case: \"index\";\n  } | {\n    /**\n     * `bool_key` specifies a map key of type bool.\n     *\n     * @generated from field: bool bool_key = 7;\n     */\n    value: boolean;\n    case: \"boolKey\";\n  } | {\n    /**\n     * `int_key` specifies a map key of type int32, int64, sint32, sint64, sfixed32 or sfixed64.\n     *\n     * @generated from field: int64 int_key = 8;\n     */\n    value: bigint;\n    case: \"intKey\";\n  } | {\n    /**\n     * `uint_key` specifies a map key of type uint32, uint64, fixed32 or fixed64.\n     *\n     * @generated from field: uint64 uint_key = 9;\n     */\n    value: bigint;\n    case: \"uintKey\";\n  } | {\n    /**\n     * `string_key` specifies a map key of type string.\n     *\n     * @generated from field: string string_key = 10;\n     */\n    value: string;\n    case: \"stringKey\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message buf.validate.FieldPathElement.\n * Use `create(FieldPathElementSchema)` to create a new message.\n */\nexport const FieldPathElementSchema: GenMessage<FieldPathElement> = /*@__PURE__*/\n  messageDesc(file_buf_validate_validate, 30);\n\n/**\n * Specifies how `FieldRules.ignore` behaves, depending on the field's value, and\n * whether the field tracks presence.\n *\n * @generated from enum buf.validate.Ignore\n */\nexport enum Ignore {\n  /**\n   * Ignore rules if the field tracks presence and is unset. This is the default\n   * behavior.\n   *\n   * In proto3, only message fields, members of a Protobuf `oneof`, and fields\n   * with the `optional` label track presence. Consequently, the following fields\n   * are always validated, whether a value is set or not:\n   *\n   * ```proto\n   * syntax=\"proto3\";\n   *\n   * message RulesApply {\n   *   string email = 1 [\n   *     (buf.validate.field).string.email = true\n   *   ];\n   *   int32 age = 2 [\n   *     (buf.validate.field).int32.gt = 0\n   *   ];\n   *   repeated string labels = 3 [\n   *     (buf.validate.field).repeated.min_items = 1\n   *   ];\n   * }\n   * ```\n   *\n   * In contrast, the following fields track presence, and are only validated if\n   * a value is set:\n   *\n   * ```proto\n   * syntax=\"proto3\";\n   *\n   * message RulesApplyIfSet {\n   *   optional string email = 1 [\n   *     (buf.validate.field).string.email = true\n   *   ];\n   *   oneof ref {\n   *     string reference = 2 [\n   *       (buf.validate.field).string.uuid = true\n   *     ];\n   *     string name = 3 [\n   *       (buf.validate.field).string.min_len = 4\n   *     ];\n   *   }\n   *   SomeMessage msg = 4 [\n   *     (buf.validate.field).cel = {/* ... *\\/}\n   *   ];\n   * }\n   * ```\n   *\n   * To ensure that such a field is set, add the `required` rule.\n   *\n   * To learn which fields track presence, see the\n   * [Field Presence cheat sheet](https://protobuf.dev/programming-guides/field_presence/#cheat).\n   *\n   * @generated from enum value: IGNORE_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * Ignore rules if the field is unset, or set to the zero value.\n   *\n   * The zero value depends on the field type:\n   * - For strings, the zero value is the empty string.\n   * - For bytes, the zero value is empty bytes.\n   * - For bool, the zero value is false.\n   * - For numeric types, the zero value is zero.\n   * - For enums, the zero value is the first defined enum value.\n   * - For repeated fields, the zero is an empty list.\n   * - For map fields, the zero is an empty map.\n   * - For message fields, absence of the message (typically a null-value) is considered zero value.\n   *\n   * For fields that track presence (e.g. adding the `optional` label in proto3),\n   * this a no-op and behavior is the same as the default `IGNORE_UNSPECIFIED`.\n   *\n   * @generated from enum value: IGNORE_IF_ZERO_VALUE = 1;\n   */\n  IF_ZERO_VALUE = 1,\n\n  /**\n   * Always ignore rules, including the `required` rule.\n   *\n   * This is useful for ignoring the rules of a referenced message, or to\n   * temporarily ignore rules during development.\n   *\n   * ```proto\n   * message MyMessage {\n   *   // The field's rules will always be ignored, including any validations\n   *   // on value's fields.\n   *   MyOtherMessage value = 1 [\n   *     (buf.validate.field).ignore = IGNORE_ALWAYS\n   *   ];\n   * }\n   * ```\n   *\n   * @generated from enum value: IGNORE_ALWAYS = 3;\n   */\n  ALWAYS = 3,\n}\n\n/**\n * Describes the enum buf.validate.Ignore.\n */\nexport const IgnoreSchema: GenEnum<Ignore> = /*@__PURE__*/\n  enumDesc(file_buf_validate_validate, 0);\n\n/**\n * KnownRegex contains some well-known patterns.\n *\n * @generated from enum buf.validate.KnownRegex\n */\nexport enum KnownRegex {\n  /**\n   * @generated from enum value: KNOWN_REGEX_UNSPECIFIED = 0;\n   */\n  UNSPECIFIED = 0,\n\n  /**\n   * HTTP header name as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2).\n   *\n   * @generated from enum value: KNOWN_REGEX_HTTP_HEADER_NAME = 1;\n   */\n  HTTP_HEADER_NAME = 1,\n\n  /**\n   * HTTP header value as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4).\n   *\n   * @generated from enum value: KNOWN_REGEX_HTTP_HEADER_VALUE = 2;\n   */\n  HTTP_HEADER_VALUE = 2,\n}\n\n/**\n * Describes the enum buf.validate.KnownRegex.\n */\nexport const KnownRegexSchema: GenEnum<KnownRegex> = /*@__PURE__*/\n  enumDesc(file_buf_validate_validate, 1);\n\n/**\n * Rules specify the validations to be performed on this message. By default,\n * no validation is performed against a message.\n *\n * @generated from extension: optional buf.validate.MessageRules message = 1159;\n */\nexport const message: GenExtension<MessageOptions, MessageRules> = /*@__PURE__*/\n  extDesc(file_buf_validate_validate, 0);\n\n/**\n * Rules specify the validations to be performed on this oneof. By default,\n * no validation is performed against a oneof.\n *\n * @generated from extension: optional buf.validate.OneofRules oneof = 1159;\n */\nexport const oneof: GenExtension<OneofOptions, OneofRules> = /*@__PURE__*/\n  extDesc(file_buf_validate_validate, 1);\n\n/**\n * Rules specify the validations to be performed on this field. By default,\n * no validation is performed against a field.\n *\n * @generated from extension: optional buf.validate.FieldRules field = 1159;\n */\nexport const field: GenExtension<FieldOptions, FieldRules> = /*@__PURE__*/\n  extDesc(file_buf_validate_validate, 2);\n\n/**\n * Specifies predefined rules. When extending a standard rule message,\n * this adds additional CEL expressions that apply when the extension is used.\n *\n * ```proto\n * extend buf.validate.Int32Rules {\n *   bool is_zero [(buf.validate.predefined).cel = {\n *     id: \"int32.is_zero\",\n *     message: \"value must be zero\",\n *     expression: \"!rule || this == 0\",\n *   }];\n * }\n *\n * message Foo {\n *   int32 reserved = 1 [(buf.validate.field).int32.(is_zero) = true];\n * }\n * ```\n *\n * @generated from extension: optional buf.validate.PredefinedRules predefined = 1160;\n */\nexport const predefined: GenExtension<FieldOptions, PredefinedRules> = /*@__PURE__*/\n  extDesc(file_buf_validate_validate, 3);\n\n"
  },
  {
    "path": "server/src/generated/ito_connect.ts",
    "content": "// @generated by protoc-gen-connect-es v1.6.1 with parameter \"target=ts,import_extension=.js\"\n// @generated from file ito.proto (package ito, syntax proto3)\n/* eslint-disable */\n// @ts-nocheck\n\nimport { AdvancedSettings, AudioChunk, CreateDictionaryItemRequest, CreateInteractionRequest, CreateNoteRequest, DeleteDictionaryItemRequest, DeleteInteractionRequest, DeleteNoteRequest, DeleteUserDataRequest, DictionaryItem, Empty, GetAdvancedSettingsRequest, GetInteractionRequest, GetNoteRequest, Interaction, ListDictionaryItemsRequest, ListDictionaryItemsResponse, ListInteractionsRequest, ListInteractionsResponse, ListNotesRequest, ListNotesResponse, Note, SubmitTimingReportsRequest, SubmitTimingReportsResponse, TranscribeStreamRequest, TranscriptionResponse, UpdateAdvancedSettingsRequest, UpdateDictionaryItemRequest, UpdateInteractionRequest, UpdateNoteRequest } from \"./ito_pb.js\";\nimport { MethodKind } from \"@bufbuild/protobuf\";\n\n/**\n * @generated from service ito.ItoService\n */\nexport const ItoService = {\n  typeName: \"ito.ItoService\",\n  methods: {\n    /**\n     * Streams audio chunks from the client and gets a single response.\n     * This is the ideal method for dictation to reduce latency and memory usage.\n     *\n     * @generated from rpc ito.ItoService.TranscribeStream\n     */\n    transcribeStream: {\n      name: \"TranscribeStream\",\n      I: AudioChunk,\n      O: TranscriptionResponse,\n      kind: MethodKind.ClientStreaming,\n    },\n    /**\n     * Enhanced streaming transcription that accepts configuration data in-stream.\n     * Config can be sent before, during, or omitted entirely. Multiple config messages\n     * are merged by the server. This allows immediate streaming without waiting for context.\n     *\n     * @generated from rpc ito.ItoService.TranscribeStreamV2\n     */\n    transcribeStreamV2: {\n      name: \"TranscribeStreamV2\",\n      I: TranscribeStreamRequest,\n      O: TranscriptionResponse,\n      kind: MethodKind.ClientStreaming,\n    },\n    /**\n     * Note Service\n     *\n     * @generated from rpc ito.ItoService.CreateNote\n     */\n    createNote: {\n      name: \"CreateNote\",\n      I: CreateNoteRequest,\n      O: Note,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.GetNote\n     */\n    getNote: {\n      name: \"GetNote\",\n      I: GetNoteRequest,\n      O: Note,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.ListNotes\n     */\n    listNotes: {\n      name: \"ListNotes\",\n      I: ListNotesRequest,\n      O: ListNotesResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.UpdateNote\n     */\n    updateNote: {\n      name: \"UpdateNote\",\n      I: UpdateNoteRequest,\n      O: Note,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.DeleteNote\n     */\n    deleteNote: {\n      name: \"DeleteNote\",\n      I: DeleteNoteRequest,\n      O: Empty,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Interaction Service\n     *\n     * @generated from rpc ito.ItoService.CreateInteraction\n     */\n    createInteraction: {\n      name: \"CreateInteraction\",\n      I: CreateInteractionRequest,\n      O: Interaction,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.GetInteraction\n     */\n    getInteraction: {\n      name: \"GetInteraction\",\n      I: GetInteractionRequest,\n      O: Interaction,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.ListInteractions\n     */\n    listInteractions: {\n      name: \"ListInteractions\",\n      I: ListInteractionsRequest,\n      O: ListInteractionsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.UpdateInteraction\n     */\n    updateInteraction: {\n      name: \"UpdateInteraction\",\n      I: UpdateInteractionRequest,\n      O: Interaction,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.DeleteInteraction\n     */\n    deleteInteraction: {\n      name: \"DeleteInteraction\",\n      I: DeleteInteractionRequest,\n      O: Empty,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Dictionary Service\n     *\n     * @generated from rpc ito.ItoService.CreateDictionaryItem\n     */\n    createDictionaryItem: {\n      name: \"CreateDictionaryItem\",\n      I: CreateDictionaryItemRequest,\n      O: DictionaryItem,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.ListDictionaryItems\n     */\n    listDictionaryItems: {\n      name: \"ListDictionaryItems\",\n      I: ListDictionaryItemsRequest,\n      O: ListDictionaryItemsResponse,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.UpdateDictionaryItem\n     */\n    updateDictionaryItem: {\n      name: \"UpdateDictionaryItem\",\n      I: UpdateDictionaryItemRequest,\n      O: DictionaryItem,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.DeleteDictionaryItem\n     */\n    deleteDictionaryItem: {\n      name: \"DeleteDictionaryItem\",\n      I: DeleteDictionaryItemRequest,\n      O: Empty,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * User Data Service\n     *\n     * @generated from rpc ito.ItoService.DeleteUserData\n     */\n    deleteUserData: {\n      name: \"DeleteUserData\",\n      I: DeleteUserDataRequest,\n      O: Empty,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * Advanced Settings Service\n     *\n     * @generated from rpc ito.ItoService.GetAdvancedSettings\n     */\n    getAdvancedSettings: {\n      name: \"GetAdvancedSettings\",\n      I: GetAdvancedSettingsRequest,\n      O: AdvancedSettings,\n      kind: MethodKind.Unary,\n    },\n    /**\n     * @generated from rpc ito.ItoService.UpdateAdvancedSettings\n     */\n    updateAdvancedSettings: {\n      name: \"UpdateAdvancedSettings\",\n      I: UpdateAdvancedSettingsRequest,\n      O: AdvancedSettings,\n      kind: MethodKind.Unary,\n    },\n  }\n} as const;\n\n/**\n * @generated from service ito.TimingService\n */\nexport const TimingService = {\n  typeName: \"ito.TimingService\",\n  methods: {\n    /**\n     * Submit timing reports for interaction analytics\n     *\n     * @generated from rpc ito.TimingService.SubmitTimingReports\n     */\n    submitTimingReports: {\n      name: \"SubmitTimingReports\",\n      I: SubmitTimingReportsRequest,\n      O: SubmitTimingReportsResponse,\n      kind: MethodKind.Unary,\n    },\n  }\n} as const;\n\n"
  },
  {
    "path": "server/src/generated/ito_pb.ts",
    "content": "// @generated by protoc-gen-es v2.7.0 with parameter \"target=ts,import_extension=.js\"\n// @generated from file ito.proto (package ito, syntax proto3)\n/* eslint-disable */\n\nimport type { GenEnum, GenFile, GenMessage, GenService } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, fileDesc, messageDesc, serviceDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport { file_buf_validate_validate } from \"./buf/validate/validate_pb.js\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file ito.proto.\n */\nexport const file_ito: GenFile = /*@__PURE__*/\n  fileDesc(\"CglpdG8ucHJvdG8SA2l0byIHCgVFbXB0eSLRAQoLQ2xpZW50RXJyb3ISDAoEY29kZRgBIAEoCRIcCgR0eXBlGAIgASgOMg4uaXRvLkVycm9yVHlwZRIPCgdtZXNzYWdlGAMgASgJEiUKCHByb3ZpZGVyGAQgASgOMhMuaXRvLkNsaWVudFByb3ZpZGVyEi4KB2RldGFpbHMYBSADKAsyHS5pdG8uQ2xpZW50RXJyb3IuRGV0YWlsc0VudHJ5Gi4KDERldGFpbHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIisKCkF1ZGlvQ2h1bmsSHQoKYXVkaW9fZGF0YRgBIAEoDEIJukgGegQYgIBAIrMBCgtDb250ZXh0SW5mbxIZCgx3aW5kb3dfdGl0bGUYASABKAlIAIgBARIVCghhcHBfbmFtZRgCIAEoCUgBiAEBEhkKDGNvbnRleHRfdGV4dBgDIAEoCUgCiAEBEh8KBG1vZGUYBCABKA4yDC5pdG8uSXRvTW9kZUgDiAEBQg8KDV93aW5kb3dfdGl0bGVCCwoJX2FwcF9uYW1lQg8KDV9jb250ZXh0X3RleHRCBwoFX21vZGUixAEKDFN0cmVhbUNvbmZpZxImCgdjb250ZXh0GAEgASgLMhAuaXRvLkNvbnRleHRJbmZvSACIAQESKwoMbGxtX3NldHRpbmdzGAIgASgLMhAuaXRvLkxsbVNldHRpbmdzSAGIAQESEgoKdm9jYWJ1bGFyeRgDIAMoCRIbCg5pbnRlcmFjdGlvbl9pZBgEIAEoCUgCiAEBQgoKCF9jb250ZXh0Qg8KDV9sbG1fc2V0dGluZ3NCEQoPX2ludGVyYWN0aW9uX2lkImoKF1RyYW5zY3JpYmVTdHJlYW1SZXF1ZXN0EiMKBmNvbmZpZxgBIAEoCzIRLml0by5TdHJlYW1Db25maWdIABIfCgphdWRpb19kYXRhGAIgASgMQgm6SAZ6BBiAgEBIAEIJCgdwYXlsb2FkIkwKFVRyYW5zY3JpcHRpb25SZXNwb25zZRISCgp0cmFuc2NyaXB0GAEgASgJEh8KBWVycm9yGAIgASgLMhAuaXRvLkNsaWVudEVycm9yIogBCgROb3RlEgoKAmlkGAEgASgJEg8KB3VzZXJfaWQYAiABKAkSFgoOaW50ZXJhY3Rpb25faWQYAyABKAkSDwoHY29udGVudBgEIAEoCRISCgpjcmVhdGVkX2F0GAUgASgJEhIKCnVwZGF0ZWRfYXQYBiABKAkSEgoKZGVsZXRlZF9hdBgHIAEoCSJIChFDcmVhdGVOb3RlUmVxdWVzdBIKCgJpZBgBIAEoCRIWCg5pbnRlcmFjdGlvbl9pZBgCIAEoCRIPCgdjb250ZW50GAMgASgJIhwKDkdldE5vdGVSZXF1ZXN0EgoKAmlkGAEgASgJIisKEExpc3ROb3Rlc1JlcXVlc3QSFwoPc2luY2VfdGltZXN0YW1wGAEgASgJIi0KEUxpc3ROb3Rlc1Jlc3BvbnNlEhgKBW5vdGVzGAEgAygLMgkuaXRvLk5vdGUiMAoRVXBkYXRlTm90ZVJlcXVlc3QSCgoCaWQYASABKAkSDwoHY29udGVudBgCIAEoCSIfChFEZWxldGVOb3RlUmVxdWVzdBIKCgJpZBgBIAEoCSL9AQoLSW50ZXJhY3Rpb24SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRINCgV0aXRsZRgDIAEoCRISCgphc3Jfb3V0cHV0GAQgASgJEhIKCmxsbV9vdXRwdXQYBSABKAkSHQoJcmF3X2F1ZGlvGAYgASgMQgq6SAd6BRiAwtcvEhMKC2R1cmF0aW9uX21zGAcgASgFEhIKCmNyZWF0ZWRfYXQYCCABKAkSEgoKdXBkYXRlZF9hdBgJIAEoCRISCgpkZWxldGVkX2F0GAogASgJEhkKDHJhd19hdWRpb19pZBgLIAEoCUgAiAEBQg8KDV9yYXdfYXVkaW9faWQikQEKGENyZWF0ZUludGVyYWN0aW9uUmVxdWVzdBIKCgJpZBgBIAEoCRINCgV0aXRsZRgCIAEoCRISCgphc3Jfb3V0cHV0GAMgASgJEhIKCmxsbV9vdXRwdXQYBCABKAkSHQoJcmF3X2F1ZGlvGAUgASgMQgq6SAd6BRiAwtcvEhMKC2R1cmF0aW9uX21zGAYgASgFIiMKFUdldEludGVyYWN0aW9uUmVxdWVzdBIKCgJpZBgBIAEoCSIyChdMaXN0SW50ZXJhY3Rpb25zUmVxdWVzdBIXCg9zaW5jZV90aW1lc3RhbXAYASABKAkiQgoYTGlzdEludGVyYWN0aW9uc1Jlc3BvbnNlEiYKDGludGVyYWN0aW9ucxgBIAMoCzIQLml0by5JbnRlcmFjdGlvbiI1ChhVcGRhdGVJbnRlcmFjdGlvblJlcXVlc3QSCgoCaWQYASABKAkSDQoFdGl0bGUYAiABKAkiJgoYRGVsZXRlSW50ZXJhY3Rpb25SZXF1ZXN0EgoKAmlkGAEgASgJIo4BCg5EaWN0aW9uYXJ5SXRlbRIKCgJpZBgBIAEoCRIPCgd1c2VyX2lkGAIgASgJEgwKBHdvcmQYAyABKAkSFQoNcHJvbnVuY2lhdGlvbhgEIAEoCRISCgpjcmVhdGVkX2F0GAUgASgJEhIKCnVwZGF0ZWRfYXQYBiABKAkSEgoKZGVsZXRlZF9hdBgHIAEoCSJOChtDcmVhdGVEaWN0aW9uYXJ5SXRlbVJlcXVlc3QSCgoCaWQYASABKAkSDAoEd29yZBgCIAEoCRIVCg1wcm9udW5jaWF0aW9uGAMgASgJIjUKGkxpc3REaWN0aW9uYXJ5SXRlbXNSZXF1ZXN0EhcKD3NpbmNlX3RpbWVzdGFtcBgBIAEoCSJBChtMaXN0RGljdGlvbmFyeUl0ZW1zUmVzcG9uc2USIgoFaXRlbXMYASADKAsyEy5pdG8uRGljdGlvbmFyeUl0ZW0iTgobVXBkYXRlRGljdGlvbmFyeUl0ZW1SZXF1ZXN0EgoKAmlkGAEgASgJEgwKBHdvcmQYAiABKAkSFQoNcHJvbnVuY2lhdGlvbhgDIAEoCSIpChtEZWxldGVEaWN0aW9uYXJ5SXRlbVJlcXVlc3QSCgoCaWQYASABKAkiFwoVRGVsZXRlVXNlckRhdGFSZXF1ZXN0Iu8DCgtMbG1TZXR0aW5ncxIWCglhc3JfbW9kZWwYASABKAlIAIgBARIZCgxhc3JfcHJvdmlkZXIYAiABKAlIAYgBARIXCgphc3JfcHJvbXB0GAMgASgJSAKIAQESGQoMbGxtX3Byb3ZpZGVyGAQgASgJSAOIAQESFgoJbGxtX21vZGVsGAUgASgJSASIAQESHAoPbGxtX3RlbXBlcmF0dXJlGAYgASgCSAWIAQESIQoUdHJhbnNjcmlwdGlvbl9wcm9tcHQYByABKAlIBogBARIbCg5lZGl0aW5nX3Byb21wdBgIIAEoCUgHiAEBEiAKE25vX3NwZWVjaF90aHJlc2hvbGQYCSABKAJICIgBARIiChVsb3dfcXVhbGl0eV90aHJlc2hvbGQYCiABKAJICYgBAUIMCgpfYXNyX21vZGVsQg8KDV9hc3JfcHJvdmlkZXJCDQoLX2Fzcl9wcm9tcHRCDwoNX2xsbV9wcm92aWRlckIMCgpfbGxtX21vZGVsQhIKEF9sbG1fdGVtcGVyYXR1cmVCFwoVX3RyYW5zY3JpcHRpb25fcHJvbXB0QhEKD19lZGl0aW5nX3Byb21wdEIWChRfbm9fc3BlZWNoX3RocmVzaG9sZEIYChZfbG93X3F1YWxpdHlfdGhyZXNob2xkIpkBChBBZHZhbmNlZFNldHRpbmdzEgoKAmlkGAEgASgJEg8KB3VzZXJfaWQYAiABKAkSEgoKY3JlYXRlZF9hdBgDIAEoCRISCgp1cGRhdGVkX2F0GAQgASgJEh0KA2xsbRgFIAEoCzIQLml0by5MbG1TZXR0aW5ncxIhCgdkZWZhdWx0GAYgASgLMhAuaXRvLkxsbVNldHRpbmdzIhwKGkdldEFkdmFuY2VkU2V0dGluZ3NSZXF1ZXN0Ij4KHVVwZGF0ZUFkdmFuY2VkU2V0dGluZ3NSZXF1ZXN0Eh0KA2xsbRgBIAEoCzIQLml0by5MbG1TZXR0aW5ncyJ3CgtUaW1pbmdFdmVudBIMCgRuYW1lGAEgASgJEhAKCHN0YXJ0X21zGAIgASgBEhMKBmVuZF9tcxgDIAEoAUgAiAEBEhgKC2R1cmF0aW9uX21zGAQgASgBSAGIAQFCCQoHX2VuZF9tc0IOCgxfZHVyYXRpb25fbXMi1gEKDFRpbWluZ1JlcG9ydBIWCg5pbnRlcmFjdGlvbl9pZBgBIAEoCRIPCgd1c2VyX2lkGAIgASgJEhAKCHBsYXRmb3JtGAMgASgJEhMKC2FwcF92ZXJzaW9uGAQgASgJEhAKCGhvc3RuYW1lGAUgASgJEhQKDGFyY2hpdGVjdHVyZRgGIAEoCRIRCgl0aW1lc3RhbXAYByABKAkSIAoGZXZlbnRzGAggAygLMhAuaXRvLlRpbWluZ0V2ZW50EhkKEXRvdGFsX2R1cmF0aW9uX21zGAkgASgBIkAKGlN1Ym1pdFRpbWluZ1JlcG9ydHNSZXF1ZXN0EiIKB3JlcG9ydHMYASADKAsyES5pdG8uVGltaW5nUmVwb3J0Ih0KG1N1Ym1pdFRpbWluZ1JlcG9ydHNSZXNwb25zZSojCgdJdG9Nb2RlEg4KClRSQU5TQ1JJQkUQABIICgRFRElUEAEqKAoOQ2xpZW50UHJvdmlkZXISCAoER1JPURAAEgwKCENFUkVCUkFTEAEqRAoJRXJyb3JUeXBlEhEKDUNPTkZJR1VSQVRJT04QABIQCgxBVkFJTEFCSUxJVFkQARIJCgVBVURJTxACEgcKA0FQSRADMpUKCgpJdG9TZXJ2aWNlEkEKEFRyYW5zY3JpYmVTdHJlYW0SDy5pdG8uQXVkaW9DaHVuaxoaLml0by5UcmFuc2NyaXB0aW9uUmVzcG9uc2UoARJQChJUcmFuc2NyaWJlU3RyZWFtVjISHC5pdG8uVHJhbnNjcmliZVN0cmVhbVJlcXVlc3QaGi5pdG8uVHJhbnNjcmlwdGlvblJlc3BvbnNlKAESLwoKQ3JlYXRlTm90ZRIWLml0by5DcmVhdGVOb3RlUmVxdWVzdBoJLml0by5Ob3RlEikKB0dldE5vdGUSEy5pdG8uR2V0Tm90ZVJlcXVlc3QaCS5pdG8uTm90ZRI6CglMaXN0Tm90ZXMSFS5pdG8uTGlzdE5vdGVzUmVxdWVzdBoWLml0by5MaXN0Tm90ZXNSZXNwb25zZRIvCgpVcGRhdGVOb3RlEhYuaXRvLlVwZGF0ZU5vdGVSZXF1ZXN0GgkuaXRvLk5vdGUSMAoKRGVsZXRlTm90ZRIWLml0by5EZWxldGVOb3RlUmVxdWVzdBoKLml0by5FbXB0eRJEChFDcmVhdGVJbnRlcmFjdGlvbhIdLml0by5DcmVhdGVJbnRlcmFjdGlvblJlcXVlc3QaEC5pdG8uSW50ZXJhY3Rpb24SPgoOR2V0SW50ZXJhY3Rpb24SGi5pdG8uR2V0SW50ZXJhY3Rpb25SZXF1ZXN0GhAuaXRvLkludGVyYWN0aW9uEk8KEExpc3RJbnRlcmFjdGlvbnMSHC5pdG8uTGlzdEludGVyYWN0aW9uc1JlcXVlc3QaHS5pdG8uTGlzdEludGVyYWN0aW9uc1Jlc3BvbnNlEkQKEVVwZGF0ZUludGVyYWN0aW9uEh0uaXRvLlVwZGF0ZUludGVyYWN0aW9uUmVxdWVzdBoQLml0by5JbnRlcmFjdGlvbhI+ChFEZWxldGVJbnRlcmFjdGlvbhIdLml0by5EZWxldGVJbnRlcmFjdGlvblJlcXVlc3QaCi5pdG8uRW1wdHkSTQoUQ3JlYXRlRGljdGlvbmFyeUl0ZW0SIC5pdG8uQ3JlYXRlRGljdGlvbmFyeUl0ZW1SZXF1ZXN0GhMuaXRvLkRpY3Rpb25hcnlJdGVtElgKE0xpc3REaWN0aW9uYXJ5SXRlbXMSHy5pdG8uTGlzdERpY3Rpb25hcnlJdGVtc1JlcXVlc3QaIC5pdG8uTGlzdERpY3Rpb25hcnlJdGVtc1Jlc3BvbnNlEk0KFFVwZGF0ZURpY3Rpb25hcnlJdGVtEiAuaXRvLlVwZGF0ZURpY3Rpb25hcnlJdGVtUmVxdWVzdBoTLml0by5EaWN0aW9uYXJ5SXRlbRJEChREZWxldGVEaWN0aW9uYXJ5SXRlbRIgLml0by5EZWxldGVEaWN0aW9uYXJ5SXRlbVJlcXVlc3QaCi5pdG8uRW1wdHkSOAoORGVsZXRlVXNlckRhdGESGi5pdG8uRGVsZXRlVXNlckRhdGFSZXF1ZXN0GgouaXRvLkVtcHR5Ek0KE0dldEFkdmFuY2VkU2V0dGluZ3MSHy5pdG8uR2V0QWR2YW5jZWRTZXR0aW5nc1JlcXVlc3QaFS5pdG8uQWR2YW5jZWRTZXR0aW5ncxJTChZVcGRhdGVBZHZhbmNlZFNldHRpbmdzEiIuaXRvLlVwZGF0ZUFkdmFuY2VkU2V0dGluZ3NSZXF1ZXN0GhUuaXRvLkFkdmFuY2VkU2V0dGluZ3MyaQoNVGltaW5nU2VydmljZRJYChNTdWJtaXRUaW1pbmdSZXBvcnRzEh8uaXRvLlN1Ym1pdFRpbWluZ1JlcG9ydHNSZXF1ZXN0GiAuaXRvLlN1Ym1pdFRpbWluZ1JlcG9ydHNSZXNwb25zZWIGcHJvdG8z\", [file_buf_validate_validate]);\n\n/**\n * General\n * -----------------------------------------------------------------\n *\n * @generated from message ito.Empty\n */\nexport type Empty = Message<\"ito.Empty\"> & {\n};\n\n/**\n * Describes the message ito.Empty.\n * Use `create(EmptySchema)` to create a new message.\n */\nexport const EmptySchema: GenMessage<Empty> = /*@__PURE__*/\n  messageDesc(file_ito, 0);\n\n/**\n * @generated from message ito.ClientError\n */\nexport type ClientError = Message<\"ito.ClientError\"> & {\n  /**\n   * @generated from field: string code = 1;\n   */\n  code: string;\n\n  /**\n   * @generated from field: ito.ErrorType type = 2;\n   */\n  type: ErrorType;\n\n  /**\n   * @generated from field: string message = 3;\n   */\n  message: string;\n\n  /**\n   * @generated from field: ito.ClientProvider provider = 4;\n   */\n  provider: ClientProvider;\n\n  /**\n   * @generated from field: map<string, string> details = 5;\n   */\n  details: { [key: string]: string };\n};\n\n/**\n * Describes the message ito.ClientError.\n * Use `create(ClientErrorSchema)` to create a new message.\n */\nexport const ClientErrorSchema: GenMessage<ClientError> = /*@__PURE__*/\n  messageDesc(file_ito, 1);\n\n/**\n * Transcription\n * -----------------------------------------------------------------\n * A chunk of audio data for streaming.\n *\n * @generated from message ito.AudioChunk\n */\nexport type AudioChunk = Message<\"ito.AudioChunk\"> & {\n  /**\n   * 1 MB limit per chunk\n   *\n   * @generated from field: bytes audio_data = 1;\n   */\n  audioData: Uint8Array;\n};\n\n/**\n * Describes the message ito.AudioChunk.\n * Use `create(AudioChunkSchema)` to create a new message.\n */\nexport const AudioChunkSchema: GenMessage<AudioChunk> = /*@__PURE__*/\n  messageDesc(file_ito, 2);\n\n/**\n * Context information for transcription.\n *\n * @generated from message ito.ContextInfo\n */\nexport type ContextInfo = Message<\"ito.ContextInfo\"> & {\n  /**\n   * @generated from field: optional string window_title = 1;\n   */\n  windowTitle?: string;\n\n  /**\n   * @generated from field: optional string app_name = 2;\n   */\n  appName?: string;\n\n  /**\n   * @generated from field: optional string context_text = 3;\n   */\n  contextText?: string;\n\n  /**\n   * @generated from field: optional ito.ItoMode mode = 4;\n   */\n  mode?: ItoMode;\n};\n\n/**\n * Describes the message ito.ContextInfo.\n * Use `create(ContextInfoSchema)` to create a new message.\n */\nexport const ContextInfoSchema: GenMessage<ContextInfo> = /*@__PURE__*/\n  messageDesc(file_ito, 3);\n\n/**\n * Configuration that can be sent in-stream for TranscribeStreamV2.\n * All fields are optional and will be merged by the server.\n * Multiple config messages received during the stream are progressively merged.\n *\n * @generated from message ito.StreamConfig\n */\nexport type StreamConfig = Message<\"ito.StreamConfig\"> & {\n  /**\n   * @generated from field: optional ito.ContextInfo context = 1;\n   */\n  context?: ContextInfo;\n\n  /**\n   * @generated from field: optional ito.LlmSettings llm_settings = 2;\n   */\n  llmSettings?: LlmSettings;\n\n  /**\n   * @generated from field: repeated string vocabulary = 3;\n   */\n  vocabulary: string[];\n\n  /**\n   * @generated from field: optional string interaction_id = 4;\n   */\n  interactionId?: string;\n};\n\n/**\n * Describes the message ito.StreamConfig.\n * Use `create(StreamConfigSchema)` to create a new message.\n */\nexport const StreamConfigSchema: GenMessage<StreamConfig> = /*@__PURE__*/\n  messageDesc(file_ito, 4);\n\n/**\n * Request message for TranscribeStreamV2.\n * Can contain either configuration or audio data.\n *\n * @generated from message ito.TranscribeStreamRequest\n */\nexport type TranscribeStreamRequest = Message<\"ito.TranscribeStreamRequest\"> & {\n  /**\n   * @generated from oneof ito.TranscribeStreamRequest.payload\n   */\n  payload: {\n    /**\n     * Configuration/context data\n     *\n     * @generated from field: ito.StreamConfig config = 1;\n     */\n    value: StreamConfig;\n    case: \"config\";\n  } | {\n    /**\n     * Audio chunk (1 MB limit)\n     *\n     * @generated from field: bytes audio_data = 2;\n     */\n    value: Uint8Array;\n    case: \"audioData\";\n  } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message ito.TranscribeStreamRequest.\n * Use `create(TranscribeStreamRequestSchema)` to create a new message.\n */\nexport const TranscribeStreamRequestSchema: GenMessage<TranscribeStreamRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 5);\n\n/**\n * The response message containing the final transcript.\n *\n * @generated from message ito.TranscriptionResponse\n */\nexport type TranscriptionResponse = Message<\"ito.TranscriptionResponse\"> & {\n  /**\n   * @generated from field: string transcript = 1;\n   */\n  transcript: string;\n\n  /**\n   * @generated from field: ito.ClientError error = 2;\n   */\n  error?: ClientError;\n};\n\n/**\n * Describes the message ito.TranscriptionResponse.\n * Use `create(TranscriptionResponseSchema)` to create a new message.\n */\nexport const TranscriptionResponseSchema: GenMessage<TranscriptionResponse> = /*@__PURE__*/\n  messageDesc(file_ito, 6);\n\n/**\n * Notes\n * -----------------------------------------------------------------\n *\n * @generated from message ito.Note\n */\nexport type Note = Message<\"ito.Note\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * @generated from field: string user_id = 2;\n   */\n  userId: string;\n\n  /**\n   * @generated from field: string interaction_id = 3;\n   */\n  interactionId: string;\n\n  /**\n   * @generated from field: string content = 4;\n   */\n  content: string;\n\n  /**\n   * @generated from field: string created_at = 5;\n   */\n  createdAt: string;\n\n  /**\n   * @generated from field: string updated_at = 6;\n   */\n  updatedAt: string;\n\n  /**\n   * @generated from field: string deleted_at = 7;\n   */\n  deletedAt: string;\n};\n\n/**\n * Describes the message ito.Note.\n * Use `create(NoteSchema)` to create a new message.\n */\nexport const NoteSchema: GenMessage<Note> = /*@__PURE__*/\n  messageDesc(file_ito, 7);\n\n/**\n * @generated from message ito.CreateNoteRequest\n */\nexport type CreateNoteRequest = Message<\"ito.CreateNoteRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * @generated from field: string interaction_id = 2;\n   */\n  interactionId: string;\n\n  /**\n   * @generated from field: string content = 3;\n   */\n  content: string;\n};\n\n/**\n * Describes the message ito.CreateNoteRequest.\n * Use `create(CreateNoteRequestSchema)` to create a new message.\n */\nexport const CreateNoteRequestSchema: GenMessage<CreateNoteRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 8);\n\n/**\n * @generated from message ito.GetNoteRequest\n */\nexport type GetNoteRequest = Message<\"ito.GetNoteRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message ito.GetNoteRequest.\n * Use `create(GetNoteRequestSchema)` to create a new message.\n */\nexport const GetNoteRequestSchema: GenMessage<GetNoteRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 9);\n\n/**\n * @generated from message ito.ListNotesRequest\n */\nexport type ListNotesRequest = Message<\"ito.ListNotesRequest\"> & {\n  /**\n   * Optional. ISO 8601 format. If not provided, fetch all.\n   *\n   * @generated from field: string since_timestamp = 1;\n   */\n  sinceTimestamp: string;\n};\n\n/**\n * Describes the message ito.ListNotesRequest.\n * Use `create(ListNotesRequestSchema)` to create a new message.\n */\nexport const ListNotesRequestSchema: GenMessage<ListNotesRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 10);\n\n/**\n * @generated from message ito.ListNotesResponse\n */\nexport type ListNotesResponse = Message<\"ito.ListNotesResponse\"> & {\n  /**\n   * @generated from field: repeated ito.Note notes = 1;\n   */\n  notes: Note[];\n};\n\n/**\n * Describes the message ito.ListNotesResponse.\n * Use `create(ListNotesResponseSchema)` to create a new message.\n */\nexport const ListNotesResponseSchema: GenMessage<ListNotesResponse> = /*@__PURE__*/\n  messageDesc(file_ito, 11);\n\n/**\n * @generated from message ito.UpdateNoteRequest\n */\nexport type UpdateNoteRequest = Message<\"ito.UpdateNoteRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * @generated from field: string content = 2;\n   */\n  content: string;\n};\n\n/**\n * Describes the message ito.UpdateNoteRequest.\n * Use `create(UpdateNoteRequestSchema)` to create a new message.\n */\nexport const UpdateNoteRequestSchema: GenMessage<UpdateNoteRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 12);\n\n/**\n * @generated from message ito.DeleteNoteRequest\n */\nexport type DeleteNoteRequest = Message<\"ito.DeleteNoteRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message ito.DeleteNoteRequest.\n * Use `create(DeleteNoteRequestSchema)` to create a new message.\n */\nexport const DeleteNoteRequestSchema: GenMessage<DeleteNoteRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 13);\n\n/**\n * Interactions\n * -----------------------------------------------------------------\n *\n * @generated from message ito.Interaction\n */\nexport type Interaction = Message<\"ito.Interaction\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * @generated from field: string user_id = 2;\n   */\n  userId: string;\n\n  /**\n   * @generated from field: string title = 3;\n   */\n  title: string;\n\n  /**\n   * JSON string\n   *\n   * @generated from field: string asr_output = 4;\n   */\n  asrOutput: string;\n\n  /**\n   * JSON string\n   *\n   * @generated from field: string llm_output = 5;\n   */\n  llmOutput: string;\n\n  /**\n   * 100 MB limit \n   *\n   * @generated from field: bytes raw_audio = 6;\n   */\n  rawAudio: Uint8Array;\n\n  /**\n   * Duration in milliseconds\n   *\n   * @generated from field: int32 duration_ms = 7;\n   */\n  durationMs: number;\n\n  /**\n   * @generated from field: string created_at = 8;\n   */\n  createdAt: string;\n\n  /**\n   * @generated from field: string updated_at = 9;\n   */\n  updatedAt: string;\n\n  /**\n   * @generated from field: string deleted_at = 10;\n   */\n  deletedAt: string;\n\n  /**\n   * UUID reference to S3 stored audio\n   *\n   * @generated from field: optional string raw_audio_id = 11;\n   */\n  rawAudioId?: string;\n};\n\n/**\n * Describes the message ito.Interaction.\n * Use `create(InteractionSchema)` to create a new message.\n */\nexport const InteractionSchema: GenMessage<Interaction> = /*@__PURE__*/\n  messageDesc(file_ito, 14);\n\n/**\n * @generated from message ito.CreateInteractionRequest\n */\nexport type CreateInteractionRequest = Message<\"ito.CreateInteractionRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * @generated from field: string title = 2;\n   */\n  title: string;\n\n  /**\n   * @generated from field: string asr_output = 3;\n   */\n  asrOutput: string;\n\n  /**\n   * @generated from field: string llm_output = 4;\n   */\n  llmOutput: string;\n\n  /**\n   * 100 MB limit\n   *\n   * @generated from field: bytes raw_audio = 5;\n   */\n  rawAudio: Uint8Array;\n\n  /**\n   * Duration in milliseconds\n   *\n   * @generated from field: int32 duration_ms = 6;\n   */\n  durationMs: number;\n};\n\n/**\n * Describes the message ito.CreateInteractionRequest.\n * Use `create(CreateInteractionRequestSchema)` to create a new message.\n */\nexport const CreateInteractionRequestSchema: GenMessage<CreateInteractionRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 15);\n\n/**\n * @generated from message ito.GetInteractionRequest\n */\nexport type GetInteractionRequest = Message<\"ito.GetInteractionRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message ito.GetInteractionRequest.\n * Use `create(GetInteractionRequestSchema)` to create a new message.\n */\nexport const GetInteractionRequestSchema: GenMessage<GetInteractionRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 16);\n\n/**\n * @generated from message ito.ListInteractionsRequest\n */\nexport type ListInteractionsRequest = Message<\"ito.ListInteractionsRequest\"> & {\n  /**\n   * Optional. ISO 8601 format. If not provided, fetch all.\n   *\n   * @generated from field: string since_timestamp = 1;\n   */\n  sinceTimestamp: string;\n};\n\n/**\n * Describes the message ito.ListInteractionsRequest.\n * Use `create(ListInteractionsRequestSchema)` to create a new message.\n */\nexport const ListInteractionsRequestSchema: GenMessage<ListInteractionsRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 17);\n\n/**\n * @generated from message ito.ListInteractionsResponse\n */\nexport type ListInteractionsResponse = Message<\"ito.ListInteractionsResponse\"> & {\n  /**\n   * @generated from field: repeated ito.Interaction interactions = 1;\n   */\n  interactions: Interaction[];\n};\n\n/**\n * Describes the message ito.ListInteractionsResponse.\n * Use `create(ListInteractionsResponseSchema)` to create a new message.\n */\nexport const ListInteractionsResponseSchema: GenMessage<ListInteractionsResponse> = /*@__PURE__*/\n  messageDesc(file_ito, 18);\n\n/**\n * @generated from message ito.UpdateInteractionRequest\n */\nexport type UpdateInteractionRequest = Message<\"ito.UpdateInteractionRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * @generated from field: string title = 2;\n   */\n  title: string;\n};\n\n/**\n * Describes the message ito.UpdateInteractionRequest.\n * Use `create(UpdateInteractionRequestSchema)` to create a new message.\n */\nexport const UpdateInteractionRequestSchema: GenMessage<UpdateInteractionRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 19);\n\n/**\n * @generated from message ito.DeleteInteractionRequest\n */\nexport type DeleteInteractionRequest = Message<\"ito.DeleteInteractionRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message ito.DeleteInteractionRequest.\n * Use `create(DeleteInteractionRequestSchema)` to create a new message.\n */\nexport const DeleteInteractionRequestSchema: GenMessage<DeleteInteractionRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 20);\n\n/**\n * Dictionary\n * -----------------------------------------------------------------\n *\n * @generated from message ito.DictionaryItem\n */\nexport type DictionaryItem = Message<\"ito.DictionaryItem\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * @generated from field: string user_id = 2;\n   */\n  userId: string;\n\n  /**\n   * @generated from field: string word = 3;\n   */\n  word: string;\n\n  /**\n   * @generated from field: string pronunciation = 4;\n   */\n  pronunciation: string;\n\n  /**\n   * @generated from field: string created_at = 5;\n   */\n  createdAt: string;\n\n  /**\n   * @generated from field: string updated_at = 6;\n   */\n  updatedAt: string;\n\n  /**\n   * @generated from field: string deleted_at = 7;\n   */\n  deletedAt: string;\n};\n\n/**\n * Describes the message ito.DictionaryItem.\n * Use `create(DictionaryItemSchema)` to create a new message.\n */\nexport const DictionaryItemSchema: GenMessage<DictionaryItem> = /*@__PURE__*/\n  messageDesc(file_ito, 21);\n\n/**\n * @generated from message ito.CreateDictionaryItemRequest\n */\nexport type CreateDictionaryItemRequest = Message<\"ito.CreateDictionaryItemRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * @generated from field: string word = 2;\n   */\n  word: string;\n\n  /**\n   * @generated from field: string pronunciation = 3;\n   */\n  pronunciation: string;\n};\n\n/**\n * Describes the message ito.CreateDictionaryItemRequest.\n * Use `create(CreateDictionaryItemRequestSchema)` to create a new message.\n */\nexport const CreateDictionaryItemRequestSchema: GenMessage<CreateDictionaryItemRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 22);\n\n/**\n * @generated from message ito.ListDictionaryItemsRequest\n */\nexport type ListDictionaryItemsRequest = Message<\"ito.ListDictionaryItemsRequest\"> & {\n  /**\n   * Optional. ISO 8601 format. If not provided, fetch all.\n   *\n   * @generated from field: string since_timestamp = 1;\n   */\n  sinceTimestamp: string;\n};\n\n/**\n * Describes the message ito.ListDictionaryItemsRequest.\n * Use `create(ListDictionaryItemsRequestSchema)` to create a new message.\n */\nexport const ListDictionaryItemsRequestSchema: GenMessage<ListDictionaryItemsRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 23);\n\n/**\n * @generated from message ito.ListDictionaryItemsResponse\n */\nexport type ListDictionaryItemsResponse = Message<\"ito.ListDictionaryItemsResponse\"> & {\n  /**\n   * @generated from field: repeated ito.DictionaryItem items = 1;\n   */\n  items: DictionaryItem[];\n};\n\n/**\n * Describes the message ito.ListDictionaryItemsResponse.\n * Use `create(ListDictionaryItemsResponseSchema)` to create a new message.\n */\nexport const ListDictionaryItemsResponseSchema: GenMessage<ListDictionaryItemsResponse> = /*@__PURE__*/\n  messageDesc(file_ito, 24);\n\n/**\n * @generated from message ito.UpdateDictionaryItemRequest\n */\nexport type UpdateDictionaryItemRequest = Message<\"ito.UpdateDictionaryItemRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * @generated from field: string word = 2;\n   */\n  word: string;\n\n  /**\n   * @generated from field: string pronunciation = 3;\n   */\n  pronunciation: string;\n};\n\n/**\n * Describes the message ito.UpdateDictionaryItemRequest.\n * Use `create(UpdateDictionaryItemRequestSchema)` to create a new message.\n */\nexport const UpdateDictionaryItemRequestSchema: GenMessage<UpdateDictionaryItemRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 25);\n\n/**\n * @generated from message ito.DeleteDictionaryItemRequest\n */\nexport type DeleteDictionaryItemRequest = Message<\"ito.DeleteDictionaryItemRequest\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n};\n\n/**\n * Describes the message ito.DeleteDictionaryItemRequest.\n * Use `create(DeleteDictionaryItemRequestSchema)` to create a new message.\n */\nexport const DeleteDictionaryItemRequestSchema: GenMessage<DeleteDictionaryItemRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 26);\n\n/**\n * User Data\n * -----------------------------------------------------------------\n *\n * Empty - user_id will be extracted from the authenticated user's token\n *\n * @generated from message ito.DeleteUserDataRequest\n */\nexport type DeleteUserDataRequest = Message<\"ito.DeleteUserDataRequest\"> & {\n};\n\n/**\n * Describes the message ito.DeleteUserDataRequest.\n * Use `create(DeleteUserDataRequestSchema)` to create a new message.\n */\nexport const DeleteUserDataRequestSchema: GenMessage<DeleteUserDataRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 27);\n\n/**\n * @generated from message ito.LlmSettings\n */\nexport type LlmSettings = Message<\"ito.LlmSettings\"> & {\n  /**\n   * @generated from field: optional string asr_model = 1;\n   */\n  asrModel?: string;\n\n  /**\n   * @generated from field: optional string asr_provider = 2;\n   */\n  asrProvider?: string;\n\n  /**\n   * @generated from field: optional string asr_prompt = 3;\n   */\n  asrPrompt?: string;\n\n  /**\n   * @generated from field: optional string llm_provider = 4;\n   */\n  llmProvider?: string;\n\n  /**\n   * @generated from field: optional string llm_model = 5;\n   */\n  llmModel?: string;\n\n  /**\n   * @generated from field: optional float llm_temperature = 6;\n   */\n  llmTemperature?: number;\n\n  /**\n   * @generated from field: optional string transcription_prompt = 7;\n   */\n  transcriptionPrompt?: string;\n\n  /**\n   * @generated from field: optional string editing_prompt = 8;\n   */\n  editingPrompt?: string;\n\n  /**\n   * @generated from field: optional float no_speech_threshold = 9;\n   */\n  noSpeechThreshold?: number;\n\n  /**\n   * @generated from field: optional float low_quality_threshold = 10;\n   */\n  lowQualityThreshold?: number;\n};\n\n/**\n * Describes the message ito.LlmSettings.\n * Use `create(LlmSettingsSchema)` to create a new message.\n */\nexport const LlmSettingsSchema: GenMessage<LlmSettings> = /*@__PURE__*/\n  messageDesc(file_ito, 28);\n\n/**\n * @generated from message ito.AdvancedSettings\n */\nexport type AdvancedSettings = Message<\"ito.AdvancedSettings\"> & {\n  /**\n   * @generated from field: string id = 1;\n   */\n  id: string;\n\n  /**\n   * @generated from field: string user_id = 2;\n   */\n  userId: string;\n\n  /**\n   * @generated from field: string created_at = 3;\n   */\n  createdAt: string;\n\n  /**\n   * @generated from field: string updated_at = 4;\n   */\n  updatedAt: string;\n\n  /**\n   * @generated from field: ito.LlmSettings llm = 5;\n   */\n  llm?: LlmSettings;\n\n  /**\n   * @generated from field: ito.LlmSettings default = 6;\n   */\n  default?: LlmSettings;\n};\n\n/**\n * Describes the message ito.AdvancedSettings.\n * Use `create(AdvancedSettingsSchema)` to create a new message.\n */\nexport const AdvancedSettingsSchema: GenMessage<AdvancedSettings> = /*@__PURE__*/\n  messageDesc(file_ito, 29);\n\n/**\n * Empty - user_id will be extracted from the authenticated user's token\n *\n * @generated from message ito.GetAdvancedSettingsRequest\n */\nexport type GetAdvancedSettingsRequest = Message<\"ito.GetAdvancedSettingsRequest\"> & {\n};\n\n/**\n * Describes the message ito.GetAdvancedSettingsRequest.\n * Use `create(GetAdvancedSettingsRequestSchema)` to create a new message.\n */\nexport const GetAdvancedSettingsRequestSchema: GenMessage<GetAdvancedSettingsRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 30);\n\n/**\n * @generated from message ito.UpdateAdvancedSettingsRequest\n */\nexport type UpdateAdvancedSettingsRequest = Message<\"ito.UpdateAdvancedSettingsRequest\"> & {\n  /**\n   * @generated from field: ito.LlmSettings llm = 1;\n   */\n  llm?: LlmSettings;\n};\n\n/**\n * Describes the message ito.UpdateAdvancedSettingsRequest.\n * Use `create(UpdateAdvancedSettingsRequestSchema)` to create a new message.\n */\nexport const UpdateAdvancedSettingsRequestSchema: GenMessage<UpdateAdvancedSettingsRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 31);\n\n/**\n * Timing Analytics\n * -----------------------------------------------------------------\n *\n * @generated from message ito.TimingEvent\n */\nexport type TimingEvent = Message<\"ito.TimingEvent\"> & {\n  /**\n   * @generated from field: string name = 1;\n   */\n  name: string;\n\n  /**\n   * @generated from field: double start_ms = 2;\n   */\n  startMs: number;\n\n  /**\n   * @generated from field: optional double end_ms = 3;\n   */\n  endMs?: number;\n\n  /**\n   * @generated from field: optional double duration_ms = 4;\n   */\n  durationMs?: number;\n};\n\n/**\n * Describes the message ito.TimingEvent.\n * Use `create(TimingEventSchema)` to create a new message.\n */\nexport const TimingEventSchema: GenMessage<TimingEvent> = /*@__PURE__*/\n  messageDesc(file_ito, 32);\n\n/**\n * @generated from message ito.TimingReport\n */\nexport type TimingReport = Message<\"ito.TimingReport\"> & {\n  /**\n   * @generated from field: string interaction_id = 1;\n   */\n  interactionId: string;\n\n  /**\n   * @generated from field: string user_id = 2;\n   */\n  userId: string;\n\n  /**\n   * @generated from field: string platform = 3;\n   */\n  platform: string;\n\n  /**\n   * @generated from field: string app_version = 4;\n   */\n  appVersion: string;\n\n  /**\n   * @generated from field: string hostname = 5;\n   */\n  hostname: string;\n\n  /**\n   * @generated from field: string architecture = 6;\n   */\n  architecture: string;\n\n  /**\n   * @generated from field: string timestamp = 7;\n   */\n  timestamp: string;\n\n  /**\n   * @generated from field: repeated ito.TimingEvent events = 8;\n   */\n  events: TimingEvent[];\n\n  /**\n   * @generated from field: double total_duration_ms = 9;\n   */\n  totalDurationMs: number;\n};\n\n/**\n * Describes the message ito.TimingReport.\n * Use `create(TimingReportSchema)` to create a new message.\n */\nexport const TimingReportSchema: GenMessage<TimingReport> = /*@__PURE__*/\n  messageDesc(file_ito, 33);\n\n/**\n * @generated from message ito.SubmitTimingReportsRequest\n */\nexport type SubmitTimingReportsRequest = Message<\"ito.SubmitTimingReportsRequest\"> & {\n  /**\n   * @generated from field: repeated ito.TimingReport reports = 1;\n   */\n  reports: TimingReport[];\n};\n\n/**\n * Describes the message ito.SubmitTimingReportsRequest.\n * Use `create(SubmitTimingReportsRequestSchema)` to create a new message.\n */\nexport const SubmitTimingReportsRequestSchema: GenMessage<SubmitTimingReportsRequest> = /*@__PURE__*/\n  messageDesc(file_ito, 34);\n\n/**\n * Empty response\n *\n * @generated from message ito.SubmitTimingReportsResponse\n */\nexport type SubmitTimingReportsResponse = Message<\"ito.SubmitTimingReportsResponse\"> & {\n};\n\n/**\n * Describes the message ito.SubmitTimingReportsResponse.\n * Use `create(SubmitTimingReportsResponseSchema)` to create a new message.\n */\nexport const SubmitTimingReportsResponseSchema: GenMessage<SubmitTimingReportsResponse> = /*@__PURE__*/\n  messageDesc(file_ito, 35);\n\n/**\n * @generated from enum ito.ItoMode\n */\nexport enum ItoMode {\n  /**\n   * @generated from enum value: TRANSCRIBE = 0;\n   */\n  TRANSCRIBE = 0,\n\n  /**\n   * @generated from enum value: EDIT = 1;\n   */\n  EDIT = 1,\n}\n\n/**\n * Describes the enum ito.ItoMode.\n */\nexport const ItoModeSchema: GenEnum<ItoMode> = /*@__PURE__*/\n  enumDesc(file_ito, 0);\n\n/**\n * Error Types\n * -----------------------------------------------------------------\n *\n * @generated from enum ito.ClientProvider\n */\nexport enum ClientProvider {\n  /**\n   * @generated from enum value: GROQ = 0;\n   */\n  GROQ = 0,\n\n  /**\n   * @generated from enum value: CEREBRAS = 1;\n   */\n  CEREBRAS = 1,\n}\n\n/**\n * Describes the enum ito.ClientProvider.\n */\nexport const ClientProviderSchema: GenEnum<ClientProvider> = /*@__PURE__*/\n  enumDesc(file_ito, 1);\n\n/**\n * @generated from enum ito.ErrorType\n */\nexport enum ErrorType {\n  /**\n   * @generated from enum value: CONFIGURATION = 0;\n   */\n  CONFIGURATION = 0,\n\n  /**\n   * @generated from enum value: AVAILABILITY = 1;\n   */\n  AVAILABILITY = 1,\n\n  /**\n   * @generated from enum value: AUDIO = 2;\n   */\n  AUDIO = 2,\n\n  /**\n   * @generated from enum value: API = 3;\n   */\n  API = 3,\n}\n\n/**\n * Describes the enum ito.ErrorType.\n */\nexport const ErrorTypeSchema: GenEnum<ErrorType> = /*@__PURE__*/\n  enumDesc(file_ito, 2);\n\n/**\n * @generated from service ito.ItoService\n */\nexport const ItoService: GenService<{\n  /**\n   * Streams audio chunks from the client and gets a single response.\n   * This is the ideal method for dictation to reduce latency and memory usage.\n   *\n   * @generated from rpc ito.ItoService.TranscribeStream\n   */\n  transcribeStream: {\n    methodKind: \"client_streaming\";\n    input: typeof AudioChunkSchema;\n    output: typeof TranscriptionResponseSchema;\n  },\n  /**\n   * Enhanced streaming transcription that accepts configuration data in-stream.\n   * Config can be sent before, during, or omitted entirely. Multiple config messages\n   * are merged by the server. This allows immediate streaming without waiting for context.\n   *\n   * @generated from rpc ito.ItoService.TranscribeStreamV2\n   */\n  transcribeStreamV2: {\n    methodKind: \"client_streaming\";\n    input: typeof TranscribeStreamRequestSchema;\n    output: typeof TranscriptionResponseSchema;\n  },\n  /**\n   * Note Service\n   *\n   * @generated from rpc ito.ItoService.CreateNote\n   */\n  createNote: {\n    methodKind: \"unary\";\n    input: typeof CreateNoteRequestSchema;\n    output: typeof NoteSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.GetNote\n   */\n  getNote: {\n    methodKind: \"unary\";\n    input: typeof GetNoteRequestSchema;\n    output: typeof NoteSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.ListNotes\n   */\n  listNotes: {\n    methodKind: \"unary\";\n    input: typeof ListNotesRequestSchema;\n    output: typeof ListNotesResponseSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.UpdateNote\n   */\n  updateNote: {\n    methodKind: \"unary\";\n    input: typeof UpdateNoteRequestSchema;\n    output: typeof NoteSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.DeleteNote\n   */\n  deleteNote: {\n    methodKind: \"unary\";\n    input: typeof DeleteNoteRequestSchema;\n    output: typeof EmptySchema;\n  },\n  /**\n   * Interaction Service\n   *\n   * @generated from rpc ito.ItoService.CreateInteraction\n   */\n  createInteraction: {\n    methodKind: \"unary\";\n    input: typeof CreateInteractionRequestSchema;\n    output: typeof InteractionSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.GetInteraction\n   */\n  getInteraction: {\n    methodKind: \"unary\";\n    input: typeof GetInteractionRequestSchema;\n    output: typeof InteractionSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.ListInteractions\n   */\n  listInteractions: {\n    methodKind: \"unary\";\n    input: typeof ListInteractionsRequestSchema;\n    output: typeof ListInteractionsResponseSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.UpdateInteraction\n   */\n  updateInteraction: {\n    methodKind: \"unary\";\n    input: typeof UpdateInteractionRequestSchema;\n    output: typeof InteractionSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.DeleteInteraction\n   */\n  deleteInteraction: {\n    methodKind: \"unary\";\n    input: typeof DeleteInteractionRequestSchema;\n    output: typeof EmptySchema;\n  },\n  /**\n   * Dictionary Service\n   *\n   * @generated from rpc ito.ItoService.CreateDictionaryItem\n   */\n  createDictionaryItem: {\n    methodKind: \"unary\";\n    input: typeof CreateDictionaryItemRequestSchema;\n    output: typeof DictionaryItemSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.ListDictionaryItems\n   */\n  listDictionaryItems: {\n    methodKind: \"unary\";\n    input: typeof ListDictionaryItemsRequestSchema;\n    output: typeof ListDictionaryItemsResponseSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.UpdateDictionaryItem\n   */\n  updateDictionaryItem: {\n    methodKind: \"unary\";\n    input: typeof UpdateDictionaryItemRequestSchema;\n    output: typeof DictionaryItemSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.DeleteDictionaryItem\n   */\n  deleteDictionaryItem: {\n    methodKind: \"unary\";\n    input: typeof DeleteDictionaryItemRequestSchema;\n    output: typeof EmptySchema;\n  },\n  /**\n   * User Data Service\n   *\n   * @generated from rpc ito.ItoService.DeleteUserData\n   */\n  deleteUserData: {\n    methodKind: \"unary\";\n    input: typeof DeleteUserDataRequestSchema;\n    output: typeof EmptySchema;\n  },\n  /**\n   * Advanced Settings Service\n   *\n   * @generated from rpc ito.ItoService.GetAdvancedSettings\n   */\n  getAdvancedSettings: {\n    methodKind: \"unary\";\n    input: typeof GetAdvancedSettingsRequestSchema;\n    output: typeof AdvancedSettingsSchema;\n  },\n  /**\n   * @generated from rpc ito.ItoService.UpdateAdvancedSettings\n   */\n  updateAdvancedSettings: {\n    methodKind: \"unary\";\n    input: typeof UpdateAdvancedSettingsRequestSchema;\n    output: typeof AdvancedSettingsSchema;\n  },\n}> = /*@__PURE__*/\n  serviceDesc(file_ito, 0);\n\n/**\n * @generated from service ito.TimingService\n */\nexport const TimingService: GenService<{\n  /**\n   * Submit timing reports for interaction analytics\n   *\n   * @generated from rpc ito.TimingService.SubmitTimingReports\n   */\n  submitTimingReports: {\n    methodKind: \"unary\";\n    input: typeof SubmitTimingReportsRequestSchema;\n    output: typeof SubmitTimingReportsResponseSchema;\n  },\n}> = /*@__PURE__*/\n  serviceDesc(file_ito, 1);\n\n"
  },
  {
    "path": "server/src/index.ts",
    "content": "import { startServer } from './server.js'\n\n// This file's only job is to start the application.\nstartServer()\n"
  },
  {
    "path": "server/src/ito.proto",
    "content": "syntax = \"proto3\";\n\npackage ito;\n\nimport \"buf/validate/validate.proto\";\n\nservice ItoService {\n  // Streams audio chunks from the client and gets a single response.\n  // This is the ideal method for dictation to reduce latency and memory usage.\n  rpc TranscribeStream(stream AudioChunk) returns (TranscriptionResponse);\n\n  // Enhanced streaming transcription that accepts configuration data in-stream.\n  // Config can be sent before, during, or omitted entirely. Multiple config messages\n  // are merged by the server. This allows immediate streaming without waiting for context.\n  rpc TranscribeStreamV2(stream TranscribeStreamRequest) returns (TranscriptionResponse);\n\n  // Note Service\n  rpc CreateNote(CreateNoteRequest) returns (Note);\n  rpc GetNote(GetNoteRequest) returns (Note);\n  rpc ListNotes(ListNotesRequest) returns (ListNotesResponse);\n  rpc UpdateNote(UpdateNoteRequest) returns (Note);\n  rpc DeleteNote(DeleteNoteRequest) returns (Empty);\n\n  // Interaction Service\n  rpc CreateInteraction(CreateInteractionRequest) returns (Interaction);\n  rpc GetInteraction(GetInteractionRequest) returns (Interaction);\n  rpc ListInteractions(ListInteractionsRequest) returns (ListInteractionsResponse);\n  rpc UpdateInteraction(UpdateInteractionRequest) returns (Interaction);\n  rpc DeleteInteraction(DeleteInteractionRequest) returns (Empty);\n\n  // Dictionary Service\n  rpc CreateDictionaryItem(CreateDictionaryItemRequest) returns (DictionaryItem);\n  rpc ListDictionaryItems(ListDictionaryItemsRequest) returns (ListDictionaryItemsResponse);\n  rpc UpdateDictionaryItem(UpdateDictionaryItemRequest) returns (DictionaryItem);\n  rpc DeleteDictionaryItem(DeleteDictionaryItemRequest) returns (Empty);\n\n  // User Data Service\n  rpc DeleteUserData(DeleteUserDataRequest) returns (Empty);\n\n  // Advanced Settings Service\n  rpc GetAdvancedSettings(GetAdvancedSettingsRequest) returns (AdvancedSettings);\n  rpc UpdateAdvancedSettings(UpdateAdvancedSettingsRequest) returns (AdvancedSettings);\n}\n\nservice TimingService {\n  // Submit timing reports for interaction analytics\n  rpc SubmitTimingReports(SubmitTimingReportsRequest) returns (SubmitTimingReportsResponse);\n}\n\n// =================================================================\n// Messages\n// =================================================================\n\n// General\n// -----------------------------------------------------------------\nmessage Empty {}\n\nenum ItoMode {\n  TRANSCRIBE = 0;\n  EDIT = 1;\n}\n\n// Error Types\n// -----------------------------------------------------------------\nenum ClientProvider {\n  GROQ = 0;\n  CEREBRAS = 1;\n}\n\nenum ErrorType {\n  CONFIGURATION = 0;\n  AVAILABILITY = 1;\n  AUDIO = 2;\n  API = 3;\n}\n\nmessage ClientError {\n  string code = 1;\n  ErrorType type = 2;\n  string message = 3;\n  ClientProvider provider = 4;\n  map<string, string> details = 5;\n}\n\n// Transcription\n// -----------------------------------------------------------------\n// A chunk of audio data for streaming.\nmessage AudioChunk {\n  bytes audio_data = 1 [(buf.validate.field).bytes.max_len = 1048576]; // 1 MB limit per chunk\n}\n\n// Context information for transcription.\nmessage ContextInfo {\n  optional string window_title = 1;\n  optional string app_name = 2;\n  optional string context_text = 3;\n  optional ItoMode mode = 4;\n}\n\n// Configuration that can be sent in-stream for TranscribeStreamV2.\n// All fields are optional and will be merged by the server.\n// Multiple config messages received during the stream are progressively merged.\nmessage StreamConfig {\n  optional ContextInfo context = 1;\n  optional LlmSettings llm_settings = 2;\n  repeated string vocabulary = 3;\n  optional string interaction_id = 4;\n}\n\n// Request message for TranscribeStreamV2.\n// Can contain either configuration or audio data.\nmessage TranscribeStreamRequest {\n  oneof payload {\n    StreamConfig config = 1;      // Configuration/context data\n    bytes audio_data = 2 [(buf.validate.field).bytes.max_len = 1048576]; // Audio chunk (1 MB limit)\n  }\n}\n\n// The response message containing the final transcript.\nmessage TranscriptionResponse {\n  string transcript = 1;\n  ClientError error = 2;\n}\n\n// Notes\n// -----------------------------------------------------------------\nmessage Note {\n  string id = 1;\n  string user_id = 2;\n  string interaction_id = 3;\n  string content = 4;\n  string created_at = 5;\n  string updated_at = 6;\n  string deleted_at = 7;\n}\n\nmessage CreateNoteRequest {\n  string id = 1;\n  string interaction_id = 2;\n  string content = 3;\n}\n\nmessage GetNoteRequest {\n  string id = 1;\n}\n\nmessage ListNotesRequest {\n  string since_timestamp = 1; // Optional. ISO 8601 format. If not provided, fetch all.\n}\n\nmessage ListNotesResponse {\n  repeated Note notes = 1;\n}\n\nmessage UpdateNoteRequest {\n  string id = 1;\n  string content = 2;\n}\n\nmessage DeleteNoteRequest {\n  string id = 1;\n}\n\n// Interactions\n// -----------------------------------------------------------------\nmessage Interaction {\n  string id = 1;\n  string user_id = 2;\n  string title = 3;\n  string asr_output = 4; // JSON string\n  string llm_output = 5; // JSON string\n  bytes raw_audio = 6 [(buf.validate.field).bytes.max_len = 100000000]; // 100 MB limit \n  int32 duration_ms = 7; // Duration in milliseconds\n  string created_at = 8;\n  string updated_at = 9;\n  string deleted_at = 10;\n  optional string raw_audio_id = 11; // UUID reference to S3 stored audio\n}\n\nmessage CreateInteractionRequest {\n  string id = 1;\n  string title = 2;\n  string asr_output = 3;\n  string llm_output = 4;\n  bytes raw_audio = 5 [(buf.validate.field).bytes.max_len = 100000000]; // 100 MB limit\n  int32 duration_ms = 6; // Duration in milliseconds\n}\n\nmessage GetInteractionRequest {\n  string id = 1;\n}\n\nmessage ListInteractionsRequest {\n  string since_timestamp = 1; // Optional. ISO 8601 format. If not provided, fetch all.\n}\n\nmessage ListInteractionsResponse {\n  repeated Interaction interactions = 1;\n}\n\nmessage UpdateInteractionRequest {\n  string id = 1;\n  string title = 2;\n}\n\nmessage DeleteInteractionRequest {\n  string id = 1;\n}\n\n// Dictionary\n// -----------------------------------------------------------------\nmessage DictionaryItem {\n  string id = 1;\n  string user_id = 2;\n  string word = 3;\n  string pronunciation = 4;\n  string created_at = 5;\n  string updated_at = 6;\n  string deleted_at = 7;\n}\n\nmessage CreateDictionaryItemRequest {\n  string id = 1;\n  string word = 2;\n  string pronunciation = 3;\n}\n\nmessage ListDictionaryItemsRequest {\n  string since_timestamp = 1; // Optional. ISO 8601 format. If not provided, fetch all.\n}\n\nmessage ListDictionaryItemsResponse {\n  repeated DictionaryItem items = 1;\n}\n\nmessage UpdateDictionaryItemRequest {\n  string id = 1;\n  string word = 2;\n  string pronunciation = 3;\n}\n\nmessage DeleteDictionaryItemRequest {\n  string id = 1;\n}\n\n// User Data\n// -----------------------------------------------------------------\nmessage DeleteUserDataRequest {\n  // Empty - user_id will be extracted from the authenticated user's token\n}\n\n// Advanced Settings\n// -----------------------------------------------------------------\n\nmessage LlmSettings {\n  optional string asr_model = 1;\n  optional string asr_provider = 2;\n  optional string asr_prompt = 3;\n  optional string llm_provider = 4;\n  optional string llm_model = 5;\n  optional float llm_temperature = 6;\n  optional string transcription_prompt = 7;\n  optional string editing_prompt = 8;\n  optional float no_speech_threshold = 9;\n  optional float low_quality_threshold = 10;\n}\n\nmessage AdvancedSettings {\n  string id = 1;\n  string user_id = 2;\n  string created_at = 3;\n  string updated_at = 4;\n  LlmSettings llm = 5;\n  LlmSettings default = 6;\n}\n\nmessage GetAdvancedSettingsRequest {\n  // Empty - user_id will be extracted from the authenticated user's token\n}\n\nmessage UpdateAdvancedSettingsRequest {\n  LlmSettings llm = 1;\n}\n\n// Timing Analytics\n// -----------------------------------------------------------------\nmessage TimingEvent {\n  string name = 1;\n  double start_ms = 2;\n  optional double end_ms = 3;\n  optional double duration_ms = 4;\n}\n\nmessage TimingReport {\n  string interaction_id = 1;\n  string user_id = 2;\n  string platform = 3;\n  string app_version = 4;\n  string hostname = 5;\n  string architecture = 6;\n  string timestamp = 7;\n  repeated TimingEvent events = 8;\n  double total_duration_ms = 9;\n}\n\nmessage SubmitTimingReportsRequest {\n  repeated TimingReport reports = 1;\n}\n\nmessage SubmitTimingReportsResponse {\n  // Empty response\n}"
  },
  {
    "path": "server/src/migrations/1722889955000_initial_schema.js",
    "content": "import { INITIAL_SCHEMA_UP, INITIAL_SCHEMA_DOWN } from './schema/initial.js'\n\n/**\n * @type {import('node-pg-migrate').ColumnDefinitions | undefined}\n */\nexport const shorthands = undefined\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const up = pgm => {\n  pgm.sql(INITIAL_SCHEMA_UP)\n}\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const down = pgm => {\n  pgm.sql(INITIAL_SCHEMA_DOWN)\n}\n"
  },
  {
    "path": "server/src/migrations/1752006262324_add-raw-audio-column.js",
    "content": "/**\n * @type {import('node-pg-migrate').ColumnDefinitions | undefined}\n */\nexport const shorthands = undefined\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const up = pgm => {\n  pgm.addColumn('interactions', {\n    raw_audio: {\n      type: 'bytea',\n      notNull: false,\n    },\n  })\n}\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const down = pgm => {\n  pgm.dropColumn('interactions', 'raw_audio')\n}\n"
  },
  {
    "path": "server/src/migrations/1752099660683_add-duration-ms.js",
    "content": "/**\n * @type {import('node-pg-migrate').ColumnDefinitions | undefined}\n */\nexport const shorthands = undefined\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const up = pgm => {\n  pgm.addColumn('interactions', {\n    duration_ms: {\n      type: 'integer',\n      default: 0,\n      notNull: false,\n    },\n  })\n}\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const down = pgm => {\n  pgm.dropColumn('interactions', 'duration_ms')\n}\n"
  },
  {
    "path": "server/src/migrations/1753297915000_add-advanced-settings.js",
    "content": "/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const up = pgm => {\n  pgm.createTable('llm_settings', {\n    id: {\n      type: 'uuid',\n      primaryKey: true,\n      default: pgm.func('gen_random_uuid()'),\n    },\n    user_id: {\n      type: 'text',\n      notNull: true,\n      unique: true,\n    },\n    asr_model: {\n      type: 'text',\n      default: 'whisper-large-v3',\n    },\n    created_at: {\n      type: 'timestamptz',\n      notNull: true,\n      default: pgm.func('current_timestamp'),\n    },\n    updated_at: {\n      type: 'timestamptz',\n      notNull: true,\n      default: pgm.func('current_timestamp'),\n    },\n  })\n\n  // Create index on user_id for fast lookups\n  pgm.createIndex('llm_settings', 'user_id')\n}\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const down = pgm => {\n  pgm.dropTable('llm_settings')\n}\n"
  },
  {
    "path": "server/src/migrations/1754938499581_update-advanced-settings.js",
    "content": "/**\n * @type {import('node-pg-migrate').ColumnDefinitions | undefined}\n */\nexport const shorthands = undefined\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const up = pgm => {\n  // Add new columns to llm_settings table\n  pgm.addColumns('llm_settings', {\n    asr_provider: {\n      type: 'text',\n      default: 'groq',\n    },\n    asr_prompt: {\n      type: 'text',\n      default: '',\n    },\n    llm_provider: {\n      type: 'text',\n      default: 'groq',\n    },\n    llm_model: {\n      type: 'text',\n      default: 'openai/gpt-oss-120b',\n    },\n    llm_temperature: {\n      type: 'decimal',\n      default: 0.1,\n    },\n    transcription_prompt: {\n      type: 'text',\n      default: '',\n    },\n    editing_prompt: {\n      type: 'text',\n      default: '',\n    },\n    no_speech_threshold: {\n      type: 'decimal',\n      default: 0.35,\n    },\n    low_quality_threshold: {\n      type: 'decimal',\n      default: -0.55,\n    },\n  })\n}\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const down = pgm => {\n  // Remove columns from llm_settings table\n  pgm.dropColumns('llm_settings', [\n    'asr_provider',\n    'asr_prompt',\n    'llm_provider',\n    'llm_model',\n    'llm_temperature',\n    'transcription_prompt',\n    'editing_prompt',\n    'no_speech_threshold',\n    'low_quality_threshold',\n  ])\n}\n"
  },
  {
    "path": "server/src/migrations/1756922843670_raw-audio-reference.js",
    "content": "/**\n * @type {import('node-pg-migrate').ColumnDefinitions | undefined}\n */\nexport const shorthands = undefined\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const up = pgm => {\n  // Add new column for storing reference UUID instead of raw audio blob\n  pgm.addColumn('interactions', {\n    raw_audio_id: {\n      type: 'uuid',\n      notNull: false,\n      comment: 'Reference to audio file stored in S3',\n    },\n  })\n}\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const down = pgm => {\n  // Drop the column\n  pgm.dropColumn('interactions', 'raw_audio_id')\n}\n"
  },
  {
    "path": "server/src/migrations/1760496947939_add-temporary-analytics-token.js",
    "content": "/**\n * @type {import('node-pg-migrate').ColumnDefinitions | undefined}\n */\nexport const shorthands = undefined\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const up = pgm => {\n  // IP correlation candidates (privacy: store only hashed IP)\n  pgm.createTable('ip_link_candidates', {\n    ip_hash: { type: 'text', notNull: true },\n    website_distinct_id: { type: 'text', notNull: true },\n    expires_at: { type: 'timestamptz', notNull: true },\n  })\n  pgm.createIndex('ip_link_candidates', ['ip_hash', 'expires_at'], {\n    ifNotExists: true,\n    name: 'ip_link_candidates_ip_exp_idx',\n  })\n}\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const down = pgm => {\n  pgm.dropTable('ip_link_candidates', { ifExists: true })\n}\n"
  },
  {
    "path": "server/src/migrations/1761765111646_add-user-trials.js",
    "content": "/**\n * @type {import('node-pg-migrate').ColumnDefinitions | undefined}\n */\nexport const shorthands = undefined\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const up = pgm => {\n  pgm.createTable('user_trials', {\n    user_id: { type: 'text', notNull: true, unique: true },\n    trial_start_at: { type: 'timestamptz' },\n    trial_end_at: { type: 'timestamptz' },\n    has_completed_trial: { type: 'boolean', notNull: true, default: false },\n    stripe_subscription_id: { type: 'text', unique: true },\n    created_at: {\n      type: 'timestamptz',\n      notNull: true,\n      default: pgm.func('current_timestamp'),\n    },\n    updated_at: {\n      type: 'timestamptz',\n      notNull: true,\n      default: pgm.func('current_timestamp'),\n    },\n  })\n\n  // Index for lookups by user\n  pgm.createIndex('user_trials', 'user_id')\n  // Index for lookups by Stripe subscription ID\n  pgm.createIndex('user_trials', 'stripe_subscription_id')\n}\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const down = pgm => {\n  pgm.dropTable('user_trials')\n}\n"
  },
  {
    "path": "server/src/migrations/1761778190395_add-user-subscriptions.js",
    "content": "/**\n * @type {import('node-pg-migrate').ColumnDefinitions | undefined}\n */\nexport const shorthands = undefined\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const up = pgm => {\n  pgm.createTable('user_subscriptions', {\n    user_id: { type: 'text', notNull: true, unique: true },\n    stripe_customer_id: { type: 'text' },\n    stripe_subscription_id: { type: 'text' },\n    subscription_start_at: { type: 'timestamptz' },\n    created_at: {\n      type: 'timestamptz',\n      notNull: true,\n      default: pgm.func('current_timestamp'),\n    },\n    updated_at: {\n      type: 'timestamptz',\n      notNull: true,\n      default: pgm.func('current_timestamp'),\n    },\n  })\n\n  pgm.createIndex('user_subscriptions', 'user_id')\n  pgm.createIndex('user_subscriptions', 'stripe_subscription_id')\n}\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const down = pgm => {\n  pgm.dropIndex('user_subscriptions', 'stripe_subscription_id')\n  pgm.dropIndex('user_subscriptions', 'user_id')\n  pgm.dropTable('user_subscriptions')\n}\n"
  },
  {
    "path": "server/src/migrations/1762468699097_add-subscription-end-at.js",
    "content": "/**\n * @type {import('node-pg-migrate').ColumnDefinitions | undefined}\n */\nexport const shorthands = undefined\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const up = pgm => {\n  pgm.addColumn('user_subscriptions', {\n    subscription_end_at: { type: 'timestamptz' },\n  })\n}\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const down = pgm => {\n  pgm.dropColumn('user_subscriptions', 'subscription_end_at')\n}\n"
  },
  {
    "path": "server/src/migrations/1763753112000_make-llm-settings-nullable.js",
    "content": "/**\n * Make all LLM settings columns nullable to support \"use defaults\" pattern.\n * This allows NULL values to indicate \"use system default\" rather than forcing\n * empty strings or zeros.\n *\n * @type {import('node-pg-migrate').ColumnDefinitions | undefined}\n */\nexport const shorthands = undefined\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const up = pgm => {\n  // Remove DEFAULT constraints and make columns nullable\n  // This allows NULL to mean \"use system defaults\" instead of forcing empty values\n\n  pgm.alterColumn('llm_settings', 'asr_model', {\n    default: null,\n    notNull: false,\n  })\n\n  pgm.alterColumn('llm_settings', 'asr_provider', {\n    default: null,\n    notNull: false,\n  })\n\n  pgm.alterColumn('llm_settings', 'asr_prompt', {\n    default: null,\n    notNull: false,\n  })\n\n  pgm.alterColumn('llm_settings', 'llm_provider', {\n    default: null,\n    notNull: false,\n  })\n\n  pgm.alterColumn('llm_settings', 'llm_model', {\n    default: null,\n    notNull: false,\n  })\n\n  pgm.alterColumn('llm_settings', 'llm_temperature', {\n    default: null,\n    notNull: false,\n  })\n\n  pgm.alterColumn('llm_settings', 'transcription_prompt', {\n    default: null,\n    notNull: false,\n  })\n\n  pgm.alterColumn('llm_settings', 'editing_prompt', {\n    default: null,\n    notNull: false,\n  })\n\n  pgm.alterColumn('llm_settings', 'no_speech_threshold', {\n    default: null,\n    notNull: false,\n  })\n\n  pgm.alterColumn('llm_settings', 'low_quality_threshold', {\n    default: null,\n    notNull: false,\n  })\n\n  // This makes existing data consistent with the new pattern\n  // Users who had defaults will now explicitly see they're using defaults\n  // Also update updated_at so the sync service knows the data changed\n  pgm.sql(`\n    UPDATE llm_settings\n    SET\n      asr_model = CASE WHEN asr_model = 'whisper-large-v3' THEN NULL ELSE asr_model END,\n      asr_provider = CASE WHEN asr_provider = 'groq' THEN NULL ELSE asr_provider END,\n      asr_prompt = CASE WHEN asr_prompt = '' THEN NULL ELSE asr_prompt END,\n      llm_provider = CASE WHEN llm_provider = 'groq' THEN NULL ELSE llm_provider END,\n      llm_model = CASE WHEN llm_model = 'openai/gpt-oss-120b' THEN NULL ELSE llm_model END,\n      llm_temperature = CASE WHEN llm_temperature = 0.1 THEN NULL ELSE llm_temperature END,\n      transcription_prompt = CASE WHEN transcription_prompt = '' THEN NULL ELSE transcription_prompt END,\n      editing_prompt = CASE WHEN editing_prompt = '' THEN NULL ELSE editing_prompt END,\n      no_speech_threshold = CASE WHEN no_speech_threshold = 0.35 THEN NULL ELSE no_speech_threshold END,\n      low_quality_threshold = CASE WHEN low_quality_threshold = -0.55 THEN NULL ELSE low_quality_threshold END,\n      updated_at = CURRENT_TIMESTAMP\n  `)\n}\n\n/**\n * @param pgm {import('node-pg-migrate').MigrationBuilder}\n * @param run {() => void | undefined}\n * @returns {Promise<void> | void}\n */\nexport const down = pgm => {\n  // Restore the original defaults\n  // Convert NULL back to the original default values\n  pgm.sql(`\n    UPDATE llm_settings\n    SET\n      asr_model = COALESCE(asr_model, 'whisper-large-v3'),\n      asr_provider = COALESCE(asr_provider, 'groq'),\n      asr_prompt = COALESCE(asr_prompt, ''),\n      llm_provider = COALESCE(llm_provider, 'groq'),\n      llm_model = COALESCE(llm_model, 'openai/gpt-oss-120b'),\n      llm_temperature = COALESCE(llm_temperature, 0.1),\n      transcription_prompt = COALESCE(transcription_prompt, ''),\n      editing_prompt = COALESCE(editing_prompt, ''),\n      no_speech_threshold = COALESCE(no_speech_threshold, 0.35),\n      low_quality_threshold = COALESCE(low_quality_threshold, -0.55)\n  `)\n\n  // Restore NOT NULL constraints and defaults\n  pgm.alterColumn('llm_settings', 'asr_model', {\n    default: 'whisper-large-v3',\n    notNull: true,\n  })\n\n  pgm.alterColumn('llm_settings', 'asr_provider', {\n    default: 'groq',\n    notNull: true,\n  })\n\n  pgm.alterColumn('llm_settings', 'asr_prompt', {\n    default: '',\n    notNull: true,\n  })\n\n  pgm.alterColumn('llm_settings', 'llm_provider', {\n    default: 'groq',\n    notNull: true,\n  })\n\n  pgm.alterColumn('llm_settings', 'llm_model', {\n    default: 'openai/gpt-oss-120b',\n    notNull: true,\n  })\n\n  pgm.alterColumn('llm_settings', 'llm_temperature', {\n    default: 0.1,\n    notNull: true,\n  })\n\n  pgm.alterColumn('llm_settings', 'transcription_prompt', {\n    default: '',\n    notNull: true,\n  })\n\n  pgm.alterColumn('llm_settings', 'editing_prompt', {\n    default: '',\n    notNull: true,\n  })\n\n  pgm.alterColumn('llm_settings', 'no_speech_threshold', {\n    default: 0.35,\n    notNull: true,\n  })\n\n  pgm.alterColumn('llm_settings', 'low_quality_threshold', {\n    default: -0.55,\n    notNull: true,\n  })\n}\n"
  },
  {
    "path": "server/src/migrations/schema/initial.js",
    "content": "export const INITIAL_SCHEMA_UP = `\n  CREATE TABLE interactions (\n    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n    user_id TEXT,\n    title TEXT,\n    asr_output JSONB,\n    llm_output JSONB,\n    created_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp,\n    updated_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp,\n    deleted_at TIMESTAMPTZ\n  );\n\n  CREATE TABLE notes (\n    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n    user_id TEXT NOT NULL,\n    interaction_id UUID REFERENCES interactions(id) ON DELETE SET NULL,\n    content TEXT NOT NULL,\n    created_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp,\n    updated_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp,\n    deleted_at TIMESTAMPTZ\n  );\n\n  CREATE TABLE dictionary_items (\n    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n    user_id TEXT NOT NULL,\n    word TEXT NOT NULL,\n    pronunciation TEXT,\n    created_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp,\n    updated_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp,\n    deleted_at TIMESTAMPTZ\n  );\n`\n\nexport const INITIAL_SCHEMA_DOWN = `\n  DROP TABLE notes;\n  DROP TABLE dictionary_items;\n  DROP TABLE interactions;\n`\n"
  },
  {
    "path": "server/src/prompts/transcription.test.ts",
    "content": "import { describe, it, expect, mock, beforeEach, afterEach } from 'bun:test'\nimport { createTranscriptionPrompt } from './transcription.js'\n\n// Mock console.log to capture logging during tests\nconst originalConsoleLog = console.log\nlet consoleLogs: string[] = []\n\ndescribe('transcription', () => {\n  beforeEach(() => {\n    // Mock console.log to capture output\n    consoleLogs = []\n    console.log = mock((message: string) => {\n      consoleLogs.push(message)\n    })\n  })\n\n  afterEach(() => {\n    // Restore original console.log\n    console.log = originalConsoleLog\n  })\n\n  describe('estimateTokenCount', () => {\n    // Since estimateTokenCount is not exported, we'll test it indirectly through createTranscriptionPrompt\n    it('should estimate tokens correctly through prompt creation', () => {\n      // Test with known input to verify token estimation logic\n      const vocabulary = ['test'] // 4 characters = 1 token\n      const result = createTranscriptionPrompt(vocabulary)\n\n      // The base prompt is \"Dictionary entries include: \" (26 chars) + \"test\" (4 chars) + \". \" (2 chars) + \"Transcribe accurately with proper punctuation and capitalization.\" (68 chars)\n      // Total: 100 characters ≈ 25 tokens, well under the 224 limit\n      expect(result).toBe('Dictionary entries include: test. ')\n      expect(consoleLogs).toHaveLength(1)\n      expect(consoleLogs[0]).toMatch(\n        /Transcription prompt: \\d+ estimated tokens/,\n      )\n    })\n  })\n\n  describe('createTranscriptionPrompt', () => {\n    it('should create prompt with empty vocabulary', () => {\n      createTranscriptionPrompt([])\n\n      expect(consoleLogs).toHaveLength(1)\n      expect(consoleLogs[0]).toMatch(\n        /Transcription prompt: \\d+ estimated tokens/,\n      )\n      expect(consoleLogs[0]).not.toContain('vocabulary truncated')\n    })\n\n    it('should create prompt with small vocabulary', () => {\n      const vocabulary = ['hello', 'world', 'test']\n      const result = createTranscriptionPrompt(vocabulary)\n\n      expect(result).toBe('Dictionary entries include: hello, world, test. ')\n      expect(consoleLogs).toHaveLength(1)\n      expect(consoleLogs[0]).toMatch(\n        /Transcription prompt: \\d+ estimated tokens/,\n      )\n      expect(consoleLogs[0]).not.toContain('vocabulary truncated')\n    })\n\n    it('should handle single vocabulary item', () => {\n      const vocabulary = ['single']\n      const result = createTranscriptionPrompt(vocabulary)\n\n      expect(result).toBe('Dictionary entries include: single. ')\n      expect(consoleLogs).toHaveLength(1)\n    })\n\n    it('should truncate vocabulary when it exceeds token limit', () => {\n      // Create a large vocabulary that will exceed the token limit\n      const largeVocabulary = Array.from({ length: 200 }, (_, i) => `word${i}`)\n      const result = createTranscriptionPrompt(largeVocabulary)\n\n      // Should still have the correct structure\n      expect(result).toStartWith('Dictionary entries include: ')\n\n      // Should have logged truncation\n      expect(consoleLogs).toHaveLength(2)\n      expect(consoleLogs[0]).toMatch(\n        /Vocabulary truncated from \\d+ to \\d+ characters/,\n      )\n      expect(consoleLogs[1]).toMatch(\n        /Transcription prompt: \\d+ estimated tokens \\(vocabulary truncated\\)/,\n      )\n\n      // Should not end with a partial word (no comma at the end before suffix)\n      const vocabPart = result.replace('Dictionary entries include: ', '')\n      expect(vocabPart).not.toEndWith(',')\n      expect(vocabPart).not.toMatch(/,\\s*$/)\n    })\n\n    it('should respect the 224 token limit', () => {\n      // Create vocabulary that would exceed limit\n      const largeVocabulary = Array.from(\n        { length: 300 },\n        (_, i) => `verylongwordthataddsmanytokens${i}`,\n      )\n      const result = createTranscriptionPrompt(largeVocabulary)\n\n      // Estimate tokens for the result (rough approximation: 1 token ≈ 4 characters)\n      const estimatedTokens = Math.ceil(result.length / 4)\n      expect(estimatedTokens).toBeLessThanOrEqual(224)\n\n      expect(consoleLogs).toHaveLength(2)\n      expect(consoleLogs[1]).toMatch(/vocabulary truncated/)\n    })\n\n    it('should maintain proper prompt structure', () => {\n      const vocabulary = ['alpha', 'beta', 'gamma']\n      const result = createTranscriptionPrompt(vocabulary)\n\n      expect(result).toStartWith('Dictionary entries include: ')\n      expect(result).toContain('alpha, beta, gamma')\n    })\n\n    it('should handle vocabulary with special characters', () => {\n      const vocabulary = ['hello-world', 'test_case', 'special@char']\n      const result = createTranscriptionPrompt(vocabulary)\n\n      expect(result).toBe(\n        'Dictionary entries include: hello-world, test_case, special@char. ',\n      )\n    })\n\n    it('should handle vocabulary with very long individual words', () => {\n      const vocabulary = ['a'.repeat(100), 'b'.repeat(50)]\n      const result = createTranscriptionPrompt(vocabulary)\n\n      // Should still create a valid prompt\n      expect(result).toStartWith('Dictionary entries include: ')\n    })\n\n    it('should properly join vocabulary with commas and spaces', () => {\n      const vocabulary = ['one', 'two', 'three', 'four']\n      const result = createTranscriptionPrompt(vocabulary)\n\n      expect(result).toBe('Dictionary entries include: one, two, three, four. ')\n    })\n\n    it('should remove incomplete last term when truncating', () => {\n      // Create a vocabulary that will need truncation\n      const vocabulary = Array.from(\n        { length: 100 },\n        (_, i) => `word${i}thisisalongword`,\n      )\n      const result = createTranscriptionPrompt(vocabulary)\n\n      if (consoleLogs.some(log => log.includes('vocabulary truncated'))) {\n        // If truncation occurred, ensure no partial words at the end\n        const vocabPart = result.replace('Dictionary entries include: ', '')\n\n        // Should not end with a comma followed by partial text\n        const lastCommaIndex = vocabPart.lastIndexOf(',')\n        if (lastCommaIndex !== -1) {\n          const afterLastComma = vocabPart.substring(lastCommaIndex + 1).trim()\n          // If there's content after the last comma, it should be a complete word\n          if (afterLastComma) {\n            expect(afterLastComma).toMatch(/^word\\d+thisisalongword\\.$/)\n          }\n        }\n      }\n    })\n\n    it('should return simple prompt when vocabulary becomes empty after processing', () => {\n      // Test edge case where vocabulary might be filtered to empty\n      const vocabulary = [''] // This should be filtered out or result in empty vocab\n      createTranscriptionPrompt(vocabulary)\n\n      // Should return just the base instruction since vocabulary is effectively empty\n      expect(consoleLogs).toHaveLength(1)\n      expect(consoleLogs[0]).toMatch(\n        /Transcription prompt: \\d+ estimated tokens/,\n      )\n      expect(consoleLogs[0]).not.toContain('vocabulary truncated')\n    })\n  })\n})\n"
  },
  {
    "path": "server/src/prompts/transcription.ts",
    "content": "/**\n * Estimates token count using rough approximation (1 token ≈ 4 characters)\n */\nfunction estimateTokenCount(text: string): number {\n  return Math.ceil(text.length / 4)\n}\n\n/**\n * Creates a transcription prompt that stays within the 224 token limit\n */\nexport function createTranscriptionPrompt(vocabulary: string[]): string {\n  const suffix = ''\n  const maxTokens = 224\n\n  // If no vocabulary, just return the base instruction\n  if (vocabulary.length === 0) {\n    const finalTokenCount = estimateTokenCount(suffix)\n    console.log(`Transcription prompt: ${finalTokenCount} estimated tokens`)\n    return suffix\n  }\n\n  const basePrompt = 'Dictionary entries include: '\n\n  // Calculate tokens for base prompt and suffix\n  const baseTokens = estimateTokenCount(basePrompt + '. ' + suffix)\n  const availableTokensForVocab = maxTokens - baseTokens\n\n  let vocabString = vocabulary.join(', ')\n  let wasTruncated = false\n\n  // Truncate vocabulary if it exceeds available tokens\n  if (estimateTokenCount(vocabString) > availableTokensForVocab) {\n    const maxVocabLength = availableTokensForVocab * 4 - 10 // Leave buffer\n    const originalLength = vocabString.length\n    vocabString = vocabString\n      .substring(0, maxVocabLength)\n      .replace(/,\\s*[^,]*$/, '') // Remove incomplete last term\n    wasTruncated = true\n    console.log(\n      `Vocabulary truncated from ${originalLength} to ${vocabString.length} characters to stay within token limit`,\n    )\n  }\n\n  // If vocabulary string is empty after processing, return just the suffix\n  if (vocabString.trim() === '') {\n    const finalTokenCount = estimateTokenCount(suffix)\n    console.log(`Transcription prompt: ${finalTokenCount} estimated tokens`)\n    return suffix\n  }\n\n  const finalPrompt = `${basePrompt}${vocabString}. ${suffix}`\n  const finalTokenCount = estimateTokenCount(finalPrompt)\n\n  console.log(\n    `Transcription prompt: ${finalTokenCount} estimated tokens${wasTruncated ? ' (vocabulary truncated)' : ''}`,\n  )\n\n  return finalPrompt\n}\n"
  },
  {
    "path": "server/src/server.ts",
    "content": "import { fastify } from 'fastify'\nimport { fastifyConnectPlugin } from '@connectrpc/connect-fastify'\nimport { createContextValues } from '@connectrpc/connect'\nimport Auth0 from '@auth0/auth0-fastify-api'\nimport itoServiceRoutes from './services/ito/itoService.js'\nimport timingServiceRoutes from './services/ito/timingService.js'\nimport { kUser } from './auth/userContext.js'\nimport { errorInterceptor } from './services/errorInterceptor.js'\nimport { loggingInterceptor } from './services/loggingInterceptor.js'\nimport { createValidationInterceptor } from './services/validationInterceptor.js'\nimport { renderCallbackPage } from './utils/renderCallback.js'\nimport dotenv from 'dotenv'\nimport { registerLoggingRoutes } from './services/logging.js'\nimport { registerAuth0Routes } from './services/auth0.js'\nimport { IpLinkRepository } from './db/repo.js'\nimport { registerTrialRoutes } from './services/trial.js'\nimport {\n  registerBillingRoutes,\n  registerBillingPublicRoutes,\n} from './services/billing.js'\nimport { registerStripeWebhook } from './services/stripeWebhook.js'\nimport cors from '@fastify/cors'\n\ndotenv.config()\n\n// Create the main server function\nexport const startServer = async () => {\n  const connectRpcServer = fastify({\n    logger: process.env.SHOW_ALL_REQUEST_LOGS === 'true',\n    trustProxy: true,\n  })\n\n  await connectRpcServer.register(cors, { origin: '*' })\n\n  // Register the Auth0 plugin\n  const REQUIRE_AUTH = process.env.REQUIRE_AUTH === 'true'\n  const CLIENT_LOG_GROUP_NAME = process.env.CLIENT_LOG_GROUP_NAME\n\n  if (REQUIRE_AUTH) {\n    const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN\n    const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE\n    const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID\n    const AUTH0_CALLBACK_URL = process.env.AUTH0_CALLBACK_URL\n\n    if (\n      !AUTH0_DOMAIN ||\n      !AUTH0_AUDIENCE ||\n      !AUTH0_CLIENT_ID ||\n      !AUTH0_CALLBACK_URL\n    ) {\n      connectRpcServer.log.error('Auth0 configuration missing in .env file')\n      process.exit(1)\n    }\n\n    await connectRpcServer.register(Auth0, {\n      domain: AUTH0_DOMAIN,\n      audience: AUTH0_AUDIENCE,\n    })\n\n    connectRpcServer.get('/login', async (request, reply) => {\n      const { state } = request.query as { state?: string }\n\n      if (!state || typeof state !== 'string') {\n        reply.status(400).send('Missing or invalid state parameter')\n        return\n      }\n\n      const redirectUrl = new URL(`https://${AUTH0_DOMAIN}/authorize`)\n      redirectUrl.searchParams.set('response_type', 'code')\n      redirectUrl.searchParams.set('client_id', AUTH0_CLIENT_ID)\n      redirectUrl.searchParams.set('redirect_uri', AUTH0_CALLBACK_URL)\n      redirectUrl.searchParams.set(\n        'scope',\n        'openid profile email offline_access',\n      )\n      redirectUrl.searchParams.set('state', state)\n\n      reply.redirect(redirectUrl.toString(), 302)\n    })\n  }\n\n  // Register Auth0 management proxy routes at the root level (no auth required)\n  await registerAuth0Routes(connectRpcServer)\n\n  // Public billing routes (no auth)\n  await registerBillingPublicRoutes(connectRpcServer)\n\n  // Stripe webhook (public)\n  await registerStripeWebhook(connectRpcServer)\n\n  // Register IP correlation candidate (from website click)\n  connectRpcServer.post('/link/register-ip', async (request, reply) => {\n    try {\n      const { websiteDistinctId } = (request.body ?? {}) as {\n        websiteDistinctId?: string\n      }\n      if (!websiteDistinctId || typeof websiteDistinctId !== 'string') {\n        reply.code(400).send({ error: 'Missing websiteDistinctId' })\n        return\n      }\n\n      // Hash IP with a server-side salt to avoid storing raw IP\n      const ip = (request.ip || '').trim()\n      const salt = process.env.IP_SALT || 'ito-default-salt'\n      const hash = await import('crypto').then(({ createHash }) =>\n        createHash('sha256').update(`${salt}:${ip}`).digest('hex'),\n      )\n\n      await IpLinkRepository.registerCandidate(hash, websiteDistinctId)\n      reply.send({ success: true })\n    } catch (error: any) {\n      connectRpcServer.log.error({ error }, 'Failed to register IP candidate')\n      reply.code(500).send({ error: 'Internal error' })\n    }\n  })\n\n  connectRpcServer.get('/link/resolve', async (request, reply) => {\n    try {\n      const ip = (request.ip || '').trim()\n      const salt = process.env.IP_SALT || 'ito-default-salt'\n      const hash = await import('crypto').then(({ createHash }) =>\n        createHash('sha256').update(`${salt}:${ip}`).digest('hex'),\n      )\n      const websiteDistinctId = await IpLinkRepository.consumeLatestForIp(hash)\n      reply.send({ websiteDistinctId: websiteDistinctId ?? null })\n      return\n    } catch (e) {\n      connectRpcServer.log.debug({ error: e }, 'IP correlation failed')\n      reply.send({ websiteDistinctId: null })\n      return\n    }\n  })\n\n  // Register Connect RPC plugin in a context that conditionally applies Auth0 authentication\n  await connectRpcServer.register(async function (fastify) {\n    // Apply Auth0 authentication to all routes in this context only if REQUIRE_AUTH is true\n    if (REQUIRE_AUTH) {\n      console.log('Authentication is ENABLED.')\n      fastify.addHook('preHandler', fastify.requireAuth())\n    } else {\n      console.log('Authentication is DISABLED.')\n    }\n\n    if (process.env.SHOW_CLIENT_LOGS === 'true') {\n      console.log('SHOW_CLIENT_LOGS is ENABLED.')\n    } else {\n      console.log('SHOW_CLIENT_LOGS is DISABLED.')\n    }\n\n    if (process.env.SHOW_ALL_REQUEST_LOGS === 'true') {\n      console.log('SHOW_ALL_REQUEST_LOGS is ENABLED.')\n    } else {\n      console.log('SHOW_ALL_REQUEST_LOGS is DISABLED.')\n    }\n\n    // Register the Connect RPC plugin with our service routes and interceptors\n    await fastify.register(fastifyConnectPlugin, {\n      routes: router => {\n        itoServiceRoutes(router)\n        timingServiceRoutes(router)\n      },\n      // Order matters: logging -> validation -> error handling\n      interceptors: [\n        loggingInterceptor,\n        createValidationInterceptor(),\n        errorInterceptor,\n      ],\n      contextValues: request => {\n        // Pass Auth0 user info from Fastify request to Connect RPC context\n        if (REQUIRE_AUTH && request.user && request.user.sub) {\n          return createContextValues().set(kUser, request.user)\n        }\n        return createContextValues()\n      },\n    })\n\n    await registerLoggingRoutes(fastify, {\n      requireAuth: REQUIRE_AUTH,\n      clientLogGroupName: CLIENT_LOG_GROUP_NAME,\n      showClientLogs: process.env.SHOW_CLIENT_LOGS === 'true',\n    })\n\n    await registerTrialRoutes(fastify, { requireAuth: REQUIRE_AUTH })\n    await registerBillingRoutes(fastify, { requireAuth: REQUIRE_AUTH })\n  })\n\n  // Error handling - this handles Fastify-level errors, not RPC errors\n  connectRpcServer.setErrorHandler((error, _, reply) => {\n    connectRpcServer.log.error(error)\n    reply.status(500).send({\n      error: 'Internal Server Error',\n      message: error.message,\n    })\n  })\n\n  // Basic REST route for health check\n  connectRpcServer.get('/', async (_, reply) => {\n    reply.type('text/plain')\n    reply.send('Welcome to the Ito Connect RPC server!')\n  })\n\n  // Callback endpoint (alternative route for same functionality)\n  connectRpcServer.get('/callback', async (request, reply) => {\n    const { code, state } = request.query as {\n      code: string\n      state: string\n    }\n\n    const html = renderCallbackPage({ code, state })\n\n    reply.type('text/html')\n    reply.send(html)\n  })\n\n  // Start the server\n  const rpcPort = 3000\n  const host = '0.0.0.0'\n\n  try {\n    await Promise.all([connectRpcServer.listen({ port: rpcPort, host })])\n    console.log(`🚀 Connect RPC server listening on ${host}:${rpcPort}`)\n  } catch (err) {\n    connectRpcServer.log.error(err)\n    process.exit(1)\n  }\n}\n"
  },
  {
    "path": "server/src/services/__tests__/helpers.ts",
    "content": "import type { FastifyInstance } from 'fastify'\nimport { fastify } from 'fastify'\n\nexport type AnyObject = Record<string, any>\n\nexport function createTestApp(): FastifyInstance {\n  return fastify()\n}\n\nexport function addAuthHook(\n  app: FastifyInstance,\n  userSub: string = 'user-123',\n): void {\n  app.addHook('preHandler', async req => {\n    ;(req as any).user = { sub: userSub }\n  })\n}\n\nexport function createTestAppWithAuth(\n  userSub: string = 'user-123',\n): FastifyInstance {\n  const app = createTestApp()\n  addAuthHook(app, userSub)\n  return app\n}\n\nexport function createEnvReset() {\n  const originalEnv = process.env\n\n  return {\n    originalEnv,\n    reset: () => {\n      process.env = originalEnv\n    },\n    set: (env: Record<string, string | undefined>) => {\n      process.env = {\n        ...originalEnv,\n        ...env,\n      }\n    },\n  }\n}\n"
  },
  {
    "path": "server/src/services/auth0.ts",
    "content": "import type { FastifyInstance } from 'fastify'\n\ntype SendVerificationBody = {\n  dbUserId?: string\n  clientId?: string\n}\n\nexport const registerAuth0Routes = async (fastify: FastifyInstance) => {\n  const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN\n  const AUTH0_MGMT_CLIENT_ID = process.env.AUTH0_MGMT_CLIENT_ID\n  const AUTH0_MGMT_CLIENT_SECRET = process.env.AUTH0_MGMT_CLIENT_SECRET\n\n  if (!AUTH0_DOMAIN) {\n    fastify.log.error('AUTH0_DOMAIN is not set')\n  }\n  if (!AUTH0_MGMT_CLIENT_ID || !AUTH0_MGMT_CLIENT_SECRET) {\n    fastify.log.warn(\n      'Auth0 management client credentials are not fully set; management routes will fail',\n    )\n  }\n\n  const getManagementToken = async (): Promise<string | null> => {\n    if (!AUTH0_DOMAIN || !AUTH0_MGMT_CLIENT_ID || !AUTH0_MGMT_CLIENT_SECRET)\n      return null\n    try {\n      const tokenUrl = `https://${AUTH0_DOMAIN}/oauth/token`\n      const res = await fetch(tokenUrl, {\n        method: 'POST',\n        headers: { 'content-type': 'application/json' },\n        body: JSON.stringify({\n          grant_type: 'client_credentials',\n          client_id: AUTH0_MGMT_CLIENT_ID,\n          client_secret: AUTH0_MGMT_CLIENT_SECRET,\n          audience: `https://${AUTH0_DOMAIN}/api/v2/`,\n        }),\n      })\n      const data: any = await res.json()\n      if (!res.ok || !data?.access_token) {\n        throw new Error(\n          data?.error_description || 'Failed to get management token',\n        )\n      }\n      return data.access_token as string\n    } catch (err) {\n      fastify.log.error({ err }, '[Auth0] getManagementToken error')\n      return null\n    }\n  }\n\n  fastify.post('/auth0/send-verification', async (request, reply) => {\n    const body = (request.body as SendVerificationBody) || {}\n    const dbUserId = body.dbUserId\n    const clientId = body.clientId\n    if (!dbUserId) {\n      reply\n        .status(400)\n        .send({ success: false, error: 'Missing user identifier' })\n      return\n    }\n\n    const token = await getManagementToken()\n    if (!token) {\n      reply\n        .status(500)\n        .send({ success: false, error: 'Missing management token' })\n      return\n    }\n\n    try {\n      const url = `https://${AUTH0_DOMAIN}/api/v2/jobs/verification-email`\n      const payload: any = {\n        user_id: dbUserId,\n      }\n      if (clientId) payload.client_id = clientId\n      const res = await fetch(url, {\n        method: 'POST',\n        headers: {\n          'content-type': 'application/json',\n          Authorization: `Bearer ${token}`,\n        },\n        body: JSON.stringify(payload),\n      })\n      let data: any\n      try {\n        data = await res.json()\n      } catch {\n        data = undefined\n      }\n      if (!res.ok) {\n        const message =\n          data?.message ||\n          data?.error_description ||\n          data?.error ||\n          `Verification request failed (${res.status})`\n        reply.status(res.status).send({ success: false, error: message })\n        return\n      }\n      reply.send({ success: true, jobId: data?.id || data?.job_id || null })\n    } catch (error: any) {\n      reply\n        .status(500)\n        .send({ success: false, error: error?.message || 'Network error' })\n    }\n  })\n\n  fastify.get('/auth0/users-by-email', async (request, reply) => {\n    const { email } = (request.query as { email?: string }) || {}\n    if (!email) {\n      reply.status(400).send({ success: false, error: 'Missing email' })\n      return\n    }\n\n    const token = await getManagementToken()\n    if (!token) {\n      reply\n        .status(500)\n        .send({ success: false, error: 'Missing management token' })\n      return\n    }\n\n    try {\n      const url = `https://${AUTH0_DOMAIN}/api/v2/users-by-email?email=${encodeURIComponent(\n        email,\n      )}`\n      const res = await fetch(url, {\n        headers: { Authorization: `Bearer ${token}` },\n      })\n      const data: any = await res.json()\n      if (!res.ok) {\n        const message =\n          data?.message || data?.error || `Lookup failed (${res.status})`\n        reply.status(res.status).send({ success: false, error: message })\n        return\n      }\n\n      const user = Array.isArray(data)\n        ? data.find(\n            (u: any) =>\n              u?.email?.toLowerCase() === email.toLowerCase() &&\n              typeof u?.user_id === 'string' &&\n              u.user_id.startsWith('auth0|'),\n          )\n        : null\n\n      if (!user) {\n        reply.send({ success: true, exists: false, verified: false })\n        return\n      }\n\n      reply.send({\n        success: true,\n        exists: true,\n        verified: !!user.email_verified,\n        dbUserId: typeof user.user_id === 'string' ? user.user_id : null,\n      })\n    } catch (error: any) {\n      reply\n        .status(500)\n        .send({ success: false, error: error?.message || 'Network error' })\n    }\n  })\n}\n"
  },
  {
    "path": "server/src/services/billing.test.ts",
    "content": "import { describe, it, expect, mock, beforeEach, afterEach } from 'bun:test'\nimport {\n  type AnyObject,\n  createTestAppWithAuth,\n  createTestApp,\n  createEnvReset,\n} from './__tests__/helpers.js'\n\nconst mockStripeState: {\n  checkoutSessionsCreate: AnyObject | null\n  checkoutSessionsRetrieve: AnyObject | null\n  subscriptionsRetrieve: AnyObject | null\n  subscriptionsUpdate: AnyObject | null\n  subscriptionsCancel: AnyObject | null\n  shouldThrow: string | null\n} = {\n  checkoutSessionsCreate: null,\n  checkoutSessionsRetrieve: null,\n  subscriptionsRetrieve: null,\n  subscriptionsUpdate: null,\n  subscriptionsCancel: null,\n  shouldThrow: null,\n}\n\nmock.module('stripe', () => {\n  class Stripe {\n    checkout: any\n    subscriptions: any\n\n    constructor(_apiKey: string) {\n      this.checkout = {\n        sessions: {\n          create: async (params: AnyObject) => {\n            if (mockStripeState.shouldThrow === 'checkout.create') {\n              mockStripeState.shouldThrow = null\n              throw new Error('Stripe API error')\n            }\n            mockStripeState.checkoutSessionsCreate = params\n            return {\n              id: 'cs_test_123',\n              url: 'https://checkout.stripe.com/test',\n              mode: 'subscription',\n              status: 'open',\n              ...mockStripeState.checkoutSessionsCreate,\n            }\n          },\n          retrieve: async (sessionId: string) => {\n            if (mockStripeState.shouldThrow === 'checkout.retrieve') {\n              mockStripeState.shouldThrow = null\n              throw new Error('Stripe API error')\n            }\n            return {\n              id: sessionId,\n              mode: 'subscription',\n              status: 'complete',\n              payment_status: 'paid',\n              customer: 'cus_test_123',\n              subscription: 'sub_test_123',\n              ...mockStripeState.checkoutSessionsRetrieve,\n            }\n          },\n        },\n      }\n\n      this.subscriptions = {\n        retrieve: async (subscriptionId: string) => {\n          if (mockStripeState.shouldThrow === 'subscriptions.retrieve') {\n            mockStripeState.shouldThrow = null\n            throw new Error('Stripe API error')\n          }\n          return {\n            id: subscriptionId,\n            items: {\n              data: [\n                {\n                  current_period_start: Math.floor(Date.now() / 1000),\n                },\n              ],\n            },\n            ...mockStripeState.subscriptionsRetrieve,\n          }\n        },\n        update: async (subscriptionId: string, params: AnyObject) => {\n          if (mockStripeState.shouldThrow === 'subscriptions.update') {\n            mockStripeState.shouldThrow = null\n            throw new Error('Stripe API error')\n          }\n          mockStripeState.subscriptionsUpdate = { subscriptionId, params }\n          const currentPeriodEnd =\n            Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60 // 30 days from now\n          return {\n            id: subscriptionId,\n            cancel_at_period_end: params.cancel_at_period_end || false,\n            current_period_end: currentPeriodEnd,\n            status: params.cancel_at_period_end ? 'active' : 'active',\n            items: {\n              data: [\n                {\n                  current_period_start: Math.floor(Date.now() / 1000),\n                },\n              ],\n            },\n            ...mockStripeState.subscriptionsRetrieve,\n          }\n        },\n        cancel: async (subscriptionId: string) => {\n          if (mockStripeState.shouldThrow === 'subscriptions.cancel') {\n            mockStripeState.shouldThrow = null\n            throw new Error('Stripe API error')\n          }\n          mockStripeState.subscriptionsCancel = { subscriptionId }\n          return { id: subscriptionId, status: 'canceled' }\n        },\n      }\n    }\n  }\n  return {\n    default: Stripe,\n    __mockStripeState: mockStripeState,\n  }\n})\n\nconst mockSubscriptionsRepo: {\n  getByUserId: AnyObject | null\n  upsertActive: AnyObject | null\n  updateSubscriptionEndAt: AnyObject | null\n} = {\n  getByUserId: null,\n  upsertActive: null,\n  updateSubscriptionEndAt: null,\n}\n\nconst mockTrialsRepo: {\n  getByUserId: AnyObject | null\n  completeTrial: boolean\n} = {\n  getByUserId: null,\n  completeTrial: false,\n}\n\nmock.module('../db/repo.js', () => {\n  return {\n    SubscriptionsRepository: {\n      getByUserId: async (userId: string) => {\n        if (mockSubscriptionsRepo.getByUserId === null) {\n          return undefined\n        }\n        if (typeof mockSubscriptionsRepo.getByUserId === 'function') {\n          return mockSubscriptionsRepo.getByUserId(userId)\n        }\n        return mockSubscriptionsRepo.getByUserId\n      },\n      upsertActive: async (\n        userId: string,\n        stripeCustomerId: string | null,\n        stripeSubscriptionId: string | null,\n        startAt: Date | null,\n        endAt?: Date | null,\n      ) => {\n        if (mockSubscriptionsRepo.upsertActive === null) {\n          return {\n            user_id: userId,\n            stripe_customer_id: stripeCustomerId,\n            stripe_subscription_id: stripeSubscriptionId,\n            subscription_start_at: startAt,\n            subscription_end_at: endAt ?? null,\n          }\n        }\n        if (typeof mockSubscriptionsRepo.upsertActive === 'function') {\n          return mockSubscriptionsRepo.upsertActive(\n            userId,\n            stripeCustomerId,\n            stripeSubscriptionId,\n            startAt,\n            endAt,\n          )\n        }\n        return mockSubscriptionsRepo.upsertActive\n      },\n      updateSubscriptionEndAt: async (userId: string, endAt: Date | null) => {\n        if (mockSubscriptionsRepo.updateSubscriptionEndAt === null) {\n          return {\n            user_id: userId,\n            subscription_end_at: endAt,\n          }\n        }\n        if (\n          typeof mockSubscriptionsRepo.updateSubscriptionEndAt === 'function'\n        ) {\n          return mockSubscriptionsRepo.updateSubscriptionEndAt(userId, endAt)\n        }\n        return mockSubscriptionsRepo.updateSubscriptionEndAt\n      },\n    },\n    TrialsRepository: {\n      getByUserId: async (userId: string) => {\n        if (mockTrialsRepo.getByUserId === null) {\n          return undefined\n        }\n        if (typeof mockTrialsRepo.getByUserId === 'function') {\n          return mockTrialsRepo.getByUserId(userId)\n        }\n        return mockTrialsRepo.getByUserId\n      },\n      completeTrial: async (userId: string) => {\n        mockTrialsRepo.completeTrial = true\n        return {\n          user_id: userId,\n          has_completed_trial: true,\n        }\n      },\n    },\n  }\n})\n\nconst envReset = createEnvReset()\n\nimport {\n  registerBillingRoutes,\n  registerBillingPublicRoutes,\n} from './billing.js'\n\ndescribe('registerBillingRoutes', () => {\n  beforeEach(() => {\n    envReset.set({\n      STRIPE_SECRET_KEY: 'sk_test_123',\n      STRIPE_PRICE_ID: 'price_test_123',\n      STRIPE_PUBLIC_BASE_URL: 'http://localhost:3000',\n      APP_PROTOCOL: 'ito-dev',\n    })\n    mockStripeState.checkoutSessionsCreate = null\n    mockStripeState.checkoutSessionsRetrieve = null\n    mockStripeState.subscriptionsRetrieve = null\n    mockStripeState.subscriptionsUpdate = null\n    mockStripeState.subscriptionsCancel = null\n    mockStripeState.shouldThrow = null\n    mockSubscriptionsRepo.getByUserId = null\n    mockSubscriptionsRepo.upsertActive = null\n    mockSubscriptionsRepo.updateSubscriptionEndAt = null\n    mockTrialsRepo.getByUserId = null\n    mockTrialsRepo.completeTrial = false\n  })\n\n  afterEach(() => {\n    envReset.reset()\n  })\n\n  describe('POST /billing/checkout', () => {\n    it('returns 401 when requireAuth is true and user is missing', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/billing/checkout',\n      })\n\n      expect(res.statusCode).toBe(401)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Unauthorized')\n      await app.close()\n    })\n\n    it('creates checkout session when authenticated', async () => {\n      const app = createTestAppWithAuth()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/billing/checkout',\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(true)\n      expect(body.url).toBe('https://checkout.stripe.com/test')\n      expect(mockStripeState.checkoutSessionsCreate).toMatchObject({\n        mode: 'subscription',\n        client_reference_id: 'user-123',\n        metadata: { user_sub: 'user-123' },\n        line_items: [\n          {\n            price: 'price_test_123',\n            quantity: 1,\n          },\n        ],\n      })\n      await app.close()\n    })\n\n    it('handles Stripe errors gracefully', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      app.addHook('preHandler', async req => {\n        ;(req as any).user = { sub: 'user-123' }\n      })\n\n      mockStripeState.shouldThrow = 'checkout.create'\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/billing/checkout',\n      })\n\n      expect(res.statusCode).toBe(500)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Stripe API error')\n      await app.close()\n    })\n  })\n\n  describe('POST /billing/confirm', () => {\n    it('returns 401 when requireAuth is true and user is missing', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/billing/confirm',\n        payload: { session_id: 'cs_test_123' },\n      })\n\n      expect(res.statusCode).toBe(401)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Unauthorized')\n      await app.close()\n    })\n\n    it('returns 400 when session_id is missing', async () => {\n      const app = createTestAppWithAuth()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/billing/confirm',\n        payload: {},\n      })\n\n      expect(res.statusCode).toBe(400)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Missing session_id')\n      await app.close()\n    })\n\n    it('returns 400 when session mode is not subscription', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      app.addHook('preHandler', async req => {\n        ;(req as any).user = { sub: 'user-123' }\n      })\n\n      mockStripeState.checkoutSessionsRetrieve = { mode: 'payment' }\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/billing/confirm',\n        payload: { session_id: 'cs_test_123' },\n      })\n\n      expect(res.statusCode).toBe(400)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Invalid session mode')\n      await app.close()\n    })\n\n    it('returns 400 when session is not completed or paid', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      app.addHook('preHandler', async req => {\n        ;(req as any).user = { sub: 'user-123' }\n      })\n\n      mockStripeState.checkoutSessionsRetrieve = {\n        mode: 'subscription',\n        status: 'open',\n        payment_status: 'unpaid',\n      }\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/billing/confirm',\n        payload: { session_id: 'cs_test_123' },\n      })\n\n      expect(res.statusCode).toBe(400)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Session not completed')\n      await app.close()\n    })\n\n    it('accepts completed session', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      app.addHook('preHandler', async req => {\n        ;(req as any).user = { sub: 'user-123' }\n      })\n\n      mockStripeState.checkoutSessionsRetrieve = {\n        mode: 'subscription',\n        status: 'complete',\n        payment_status: 'unpaid',\n      }\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/billing/confirm',\n        payload: { session_id: 'cs_test_123' },\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(true)\n      expect(body.pro_status).toBe('active_pro')\n      expect(mockTrialsRepo.completeTrial).toBe(true)\n      await app.close()\n    })\n\n    it('accepts paid session', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      app.addHook('preHandler', async req => {\n        ;(req as any).user = { sub: 'user-123' }\n      })\n\n      mockStripeState.checkoutSessionsRetrieve = {\n        mode: 'subscription',\n        status: 'open',\n        payment_status: 'paid',\n      }\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/billing/confirm',\n        payload: { session_id: 'cs_test_123' },\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(true)\n      expect(body.pro_status).toBe('active_pro')\n      await app.close()\n    })\n\n    it('throws error when session missing customer ID', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      app.addHook('preHandler', async req => {\n        ;(req as any).user = { sub: 'user-123' }\n      })\n\n      mockStripeState.checkoutSessionsRetrieve = {\n        mode: 'subscription',\n        status: 'complete',\n        payment_status: 'paid',\n        customer: null,\n        subscription: 'sub_test_123',\n      }\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/billing/confirm',\n        payload: { session_id: 'cs_test_123' },\n      })\n\n      expect(res.statusCode).toBe(500)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Session missing customer ID')\n      await app.close()\n    })\n\n    it('throws error when session missing subscription ID', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      app.addHook('preHandler', async req => {\n        ;(req as any).user = { sub: 'user-123' }\n      })\n\n      mockStripeState.checkoutSessionsRetrieve = {\n        mode: 'subscription',\n        status: 'complete',\n        payment_status: 'paid',\n        customer: 'cus_test_123',\n        subscription: null,\n      }\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/billing/confirm',\n        payload: { session_id: 'cs_test_123' },\n      })\n\n      expect(res.statusCode).toBe(500)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Session missing subscription ID')\n      await app.close()\n    })\n\n    it('throws error when subscription missing current_period_start', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      app.addHook('preHandler', async req => {\n        ;(req as any).user = { sub: 'user-123' }\n      })\n\n      mockStripeState.checkoutSessionsRetrieve = {\n        mode: 'subscription',\n        status: 'complete',\n        payment_status: 'paid',\n        customer: 'cus_test_123',\n        subscription: 'sub_test_123',\n      }\n\n      mockStripeState.subscriptionsRetrieve = {\n        id: 'sub_test_123',\n        items: {\n          data: [],\n        },\n      }\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/billing/confirm',\n        payload: { session_id: 'cs_test_123' },\n      })\n\n      expect(res.statusCode).toBe(500)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Subscription missing current_period_start')\n      await app.close()\n    })\n\n    it('handles Stripe errors gracefully', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      app.addHook('preHandler', async req => {\n        ;(req as any).user = { sub: 'user-123' }\n      })\n\n      mockStripeState.shouldThrow = 'checkout.retrieve'\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/billing/confirm',\n        payload: { session_id: 'cs_test_123' },\n      })\n\n      expect(res.statusCode).toBe(500)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Stripe API error')\n      await app.close()\n    })\n  })\n\n  describe('POST /billing/cancel', () => {\n    it('returns 401 when requireAuth is true and user is missing', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/billing/cancel',\n      })\n\n      expect(res.statusCode).toBe(401)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Unauthorized')\n      await app.close()\n    })\n\n    it('returns 400 when no subscription is found', async () => {\n      const app = createTestAppWithAuth()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/billing/cancel',\n      })\n\n      expect(res.statusCode).toBe(400)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('No active subscription found')\n      await app.close()\n    })\n\n    it('returns 400 when subscription has no stripe_subscription_id', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      app.addHook('preHandler', async req => {\n        ;(req as any).user = { sub: 'user-123' }\n      })\n\n      mockSubscriptionsRepo.getByUserId = {\n        user_id: 'user-123',\n        stripe_subscription_id: null,\n      }\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/billing/cancel',\n      })\n\n      expect(res.statusCode).toBe(400)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('No active subscription found')\n      await app.close()\n    })\n\n    it('cancels subscription successfully', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      app.addHook('preHandler', async req => {\n        ;(req as any).user = { sub: 'user-123' }\n      })\n\n      mockSubscriptionsRepo.getByUserId = {\n        user_id: 'user-123',\n        stripe_subscription_id: 'sub_test_123',\n      }\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/billing/cancel',\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(true)\n      expect(mockStripeState.subscriptionsUpdate).toEqual({\n        subscriptionId: 'sub_test_123',\n        params: { cancel_at_period_end: true },\n      })\n      await app.close()\n    })\n\n    it('handles Stripe errors gracefully', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      app.addHook('preHandler', async req => {\n        ;(req as any).user = { sub: 'user-123' }\n      })\n\n      mockSubscriptionsRepo.getByUserId = {\n        user_id: 'user-123',\n        stripe_subscription_id: 'sub_test_123',\n      }\n\n      mockStripeState.shouldThrow = 'subscriptions.update'\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/billing/cancel',\n      })\n\n      expect(res.statusCode).toBe(500)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Stripe API error')\n      await app.close()\n    })\n  })\n\n  describe('POST /billing/reactivate', () => {\n    it('returns 401 when requireAuth is true and user is missing', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/billing/reactivate',\n      })\n\n      expect(res.statusCode).toBe(401)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Unauthorized')\n      await app.close()\n    })\n\n    it('reactivates subscription successfully', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      app.addHook('preHandler', async req => {\n        ;(req as any).user = { sub: 'user-123' }\n      })\n\n      mockSubscriptionsRepo.getByUserId = {\n        user_id: 'user-123',\n        stripe_subscription_id: 'sub_test_123',\n      }\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/billing/reactivate',\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(true)\n      expect(mockStripeState.subscriptionsUpdate).toEqual({\n        subscriptionId: 'sub_test_123',\n        params: { cancel_at_period_end: false },\n      })\n      await app.close()\n    })\n  })\n\n  describe('GET /billing/status', () => {\n    it('returns 401 when requireAuth is true and user is missing', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      const res = await app.inject({\n        method: 'GET',\n        url: '/billing/status',\n      })\n\n      expect(res.statusCode).toBe(401)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Unauthorized')\n      await app.close()\n    })\n\n    it('returns active_pro when subscription exists', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      app.addHook('preHandler', async req => {\n        ;(req as any).user = { sub: 'user-123' }\n      })\n\n      const startDate = new Date('2024-01-01')\n      mockSubscriptionsRepo.getByUserId = {\n        user_id: 'user-123',\n        stripe_subscription_id: 'sub_test_123',\n        subscription_start_at: startDate,\n        subscription_end_at: null,\n      }\n\n      const res = await app.inject({\n        method: 'GET',\n        url: '/billing/status',\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(true)\n      expect(body.pro_status).toBe('active_pro')\n      expect(new Date(body.subscriptionStartAt).getTime()).toBe(\n        startDate.getTime(),\n      )\n      expect(body.subscriptionEndAt).toBe(null)\n      expect(body.isScheduledForCancellation).toBe(false)\n      expect(body.trial).toBeDefined()\n      await app.close()\n    })\n\n    it('returns subscription_end_at and isScheduledForCancellation when scheduled for cancellation', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      app.addHook('preHandler', async req => {\n        ;(req as any).user = { sub: 'user-123' }\n      })\n\n      const startDate = new Date('2024-01-01')\n      const endDate = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days from now\n      mockSubscriptionsRepo.getByUserId = {\n        user_id: 'user-123',\n        stripe_subscription_id: 'sub_test_123',\n        subscription_start_at: startDate,\n        subscription_end_at: endDate,\n      }\n\n      const res = await app.inject({\n        method: 'GET',\n        url: '/billing/status',\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(true)\n      expect(body.pro_status).toBe('active_pro')\n      expect(new Date(body.subscriptionStartAt).getTime()).toBe(\n        startDate.getTime(),\n      )\n      expect(new Date(body.subscriptionEndAt).getTime()).toBe(endDate.getTime())\n      expect(body.isScheduledForCancellation).toBe(true)\n      await app.close()\n    })\n\n    it('returns free_trial when trial is active', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      app.addHook('preHandler', async req => {\n        ;(req as any).user = { sub: 'user-123' }\n      })\n\n      const trialStart = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000)\n      const trialEnd = new Date(Date.now() + 9 * 24 * 60 * 60 * 1000) // 9 days from now\n      mockTrialsRepo.getByUserId = {\n        user_id: 'user-123',\n        trial_start_at: trialStart,\n        trial_end_at: trialEnd,\n        has_completed_trial: false,\n      }\n\n      const res = await app.inject({\n        method: 'GET',\n        url: '/billing/status',\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(true)\n      expect(body.pro_status).toBe('free_trial')\n      expect(body.trial.isTrialActive).toBe(true)\n      expect(body.trial.daysLeft).toBeGreaterThan(0)\n      expect(body.trial.daysLeft).toBeLessThanOrEqual(14)\n      await app.close()\n    })\n\n    it('returns none when no subscription and trial expired', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      app.addHook('preHandler', async req => {\n        ;(req as any).user = { sub: 'user-123' }\n      })\n\n      const trialStart = new Date(Date.now() - 20 * 24 * 60 * 60 * 1000)\n      const trialEnd = new Date(Date.now() - 6 * 24 * 60 * 60 * 1000) // 6 days ago (expired)\n      mockTrialsRepo.getByUserId = {\n        user_id: 'user-123',\n        trial_start_at: trialStart,\n        trial_end_at: trialEnd,\n        has_completed_trial: false,\n      }\n\n      const res = await app.inject({\n        method: 'GET',\n        url: '/billing/status',\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(true)\n      expect(body.pro_status).toBe('none')\n      expect(body.trial.isTrialActive).toBe(false)\n      expect(body.trial.daysLeft).toBe(0)\n      await app.close()\n    })\n\n    it('returns none when no subscription and trial completed', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      app.addHook('preHandler', async req => {\n        ;(req as any).user = { sub: 'user-123' }\n      })\n\n      const trialStart = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000)\n      mockTrialsRepo.getByUserId = {\n        user_id: 'user-123',\n        trial_start_at: trialStart,\n        has_completed_trial: true,\n      }\n\n      const res = await app.inject({\n        method: 'GET',\n        url: '/billing/status',\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(true)\n      expect(body.pro_status).toBe('none')\n      expect(body.trial.isTrialActive).toBe(false)\n      await app.close()\n    })\n\n    it('handles errors gracefully', async () => {\n      const app = createTestApp()\n      await registerBillingRoutes(app, { requireAuth: true })\n\n      app.addHook('preHandler', async req => {\n        ;(req as any).user = { sub: 'user-123' }\n      })\n\n      mockSubscriptionsRepo.getByUserId = () => {\n        throw new Error('Database error')\n      }\n\n      const res = await app.inject({\n        method: 'GET',\n        url: '/billing/status',\n      })\n\n      expect(res.statusCode).toBe(500)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Database error')\n      await app.close()\n    })\n  })\n})\n\ndescribe('registerBillingPublicRoutes', () => {\n  beforeEach(() => {\n    envReset.set({\n      APP_PROTOCOL: 'ito-dev',\n    })\n  })\n\n  afterEach(() => {\n    envReset.reset()\n  })\n\n  it('renders success page with session_id', async () => {\n    const app = createTestApp()\n    await registerBillingPublicRoutes(app)\n\n    const res = await app.inject({\n      method: 'GET',\n      url: '/billing/success?session_id=cs_test_123',\n    })\n\n    expect(res.statusCode).toBe(200)\n    expect(res.headers['content-type']).toContain('text/html')\n    expect(res.body).toContain(\n      'ito-dev://billing/success?session_id=cs_test_123',\n    )\n    expect(res.body).toContain('Returning to Ito')\n    await app.close()\n  })\n\n  it('renders success page without session_id', async () => {\n    const app = createTestApp()\n    await registerBillingPublicRoutes(app)\n\n    const res = await app.inject({\n      method: 'GET',\n      url: '/billing/success',\n    })\n\n    expect(res.statusCode).toBe(200)\n    expect(res.headers['content-type']).toContain('text/html')\n    expect(res.body).toContain('ito-dev://billing/success')\n    expect(res.body).not.toContain('session_id=')\n    await app.close()\n  })\n\n  it('renders cancel page', async () => {\n    const app = createTestApp()\n    await registerBillingPublicRoutes(app)\n\n    const res = await app.inject({\n      method: 'GET',\n      url: '/billing/cancel',\n    })\n\n    expect(res.statusCode).toBe(200)\n    expect(res.headers['content-type']).toContain('text/html')\n    expect(res.body).toContain('ito-dev://billing/cancel')\n    expect(res.body).toContain('Returning to Ito')\n    await app.close()\n  })\n\n  it('escapes quotes in deeplink URL', async () => {\n    const app = createTestApp()\n    await registerBillingPublicRoutes(app)\n\n    process.env.APP_PROTOCOL = 'ito-dev'\n\n    const res = await app.inject({\n      method: 'GET',\n      url: '/billing/success?session_id=test\"quote',\n    })\n\n    expect(res.statusCode).toBe(200)\n    expect(res.body).toContain('test%22quote')\n    expect(res.body).not.toContain('test\"quote')\n    await app.close()\n  })\n})\n"
  },
  {
    "path": "server/src/services/billing.ts",
    "content": "import { FastifyInstance } from 'fastify'\nimport Stripe from 'stripe'\nimport { SubscriptionsRepository, TrialsRepository } from '../db/repo.js'\nimport {\n  getAuth0ManagementToken,\n  getUserInfoFromAuth0,\n} from '../auth/auth0Helpers.js'\n\ntype Options = {\n  requireAuth: boolean\n}\n\nfunction getEnv(name: string): string {\n  const v = process.env[name]\n  if (!v) throw new Error(`Missing required env var: ${name}`)\n  return v\n}\n\nfunction renderDeepLinkHtml(targetUrl: string): string {\n  const escaped = targetUrl.replace(/\"/g, '&quot;')\n  return `<!doctype html><html><head><meta charset=\"utf-8\"><title>Returning to Ito…</title></head><body>\n  <p>If you are not redirected automatically, click below:</p>\n  <a href=\"${escaped}\">Return to Ito</a>\n  <script>window.location = \"${escaped}\";</script>\n  </body></html>`\n}\n\nexport const registerBillingRoutes = async (\n  fastify: FastifyInstance,\n  options: Options,\n) => {\n  const { requireAuth } = options\n\n  const STRIPE_SECRET_KEY = getEnv('STRIPE_SECRET_KEY')\n  const STRIPE_PRICE_ID = getEnv('STRIPE_PRICE_ID')\n  const STRIPE_PUBLIC_BASE_URL = getEnv('STRIPE_PUBLIC_BASE_URL') // e.g., http://localhost:3000 or https://api.domain\n\n  const stripe = new Stripe(STRIPE_SECRET_KEY)\n\n  fastify.post('/billing/checkout', async (request, reply) => {\n    console.log('billing/checkout', request.body)\n    try {\n      const userSub = (requireAuth && (request as any).user?.sub) || undefined\n      if (!userSub) {\n        reply.code(401).send({ success: false, error: 'Unauthorized' })\n        return\n      }\n\n      // Get user info from Auth0 Management API (access token only has 'sub')\n      const auth0UserInfo = await getUserInfoFromAuth0(userSub)\n      const userEmail = auth0UserInfo?.email\n\n      const session = await stripe.checkout.sessions.create({\n        mode: 'subscription',\n        client_reference_id: userSub,\n        customer_email: userEmail,\n        metadata: { user_sub: userSub },\n        subscription_data: {\n          metadata: { user_sub: userSub },\n        },\n        line_items: [\n          {\n            price: STRIPE_PRICE_ID,\n            quantity: 1,\n          },\n        ],\n        success_url: `${STRIPE_PUBLIC_BASE_URL}/billing/success?session_id={CHECKOUT_SESSION_ID}`,\n        cancel_url: `${STRIPE_PUBLIC_BASE_URL}/billing/cancel`,\n      })\n\n      reply.send({ success: true, url: session.url })\n    } catch (error: any) {\n      fastify.log.error({ err: error }, 'Stripe checkout creation failed')\n      reply\n        .code(500)\n        .send({ success: false, error: error?.message || 'Server error' })\n    }\n  })\n\n  fastify.post('/billing/confirm', async (request, reply) => {\n    try {\n      const userSub = (requireAuth && (request as any).user?.sub) || undefined\n      if (!userSub) {\n        reply.code(401).send({ success: false, error: 'Unauthorized' })\n        return\n      }\n\n      const body = request.body as { session_id?: string }\n      const sessionId = body?.session_id\n      if (!sessionId) {\n        reply.code(400).send({ success: false, error: 'Missing session_id' })\n        return\n      }\n\n      const session = await stripe.checkout.sessions.retrieve(sessionId)\n      if (session.mode !== 'subscription') {\n        reply.code(400).send({ success: false, error: 'Invalid session mode' })\n        return\n      }\n\n      // Accept both fully completed and paid statuses\n      const isCompleted = session.status === 'complete'\n      const isPaid = session.payment_status === 'paid'\n      if (!isCompleted && !isPaid) {\n        reply.code(400).send({ success: false, error: 'Session not completed' })\n        return\n      }\n\n      if (!session.customer || typeof session.customer !== 'string') {\n        throw new Error('Session missing customer ID')\n      }\n\n      if (!session.subscription || typeof session.subscription !== 'string') {\n        throw new Error('Session missing subscription ID')\n      }\n\n      const stripeCustomerId = session.customer\n      const stripeSubscriptionId = session.subscription\n\n      // Update customer with name from Auth0 (access token only has 'sub')\n      const auth0UserInfo = await getUserInfoFromAuth0(userSub)\n      if (auth0UserInfo?.name && stripeCustomerId) {\n        await stripe.customers.update(stripeCustomerId, {\n          name: auth0UserInfo.name,\n          metadata: { user_sub: userSub },\n        })\n      }\n\n      // Check if subscription already exists (idempotency check)\n      const existingSub = await SubscriptionsRepository.getByUserId(userSub)\n      if (existingSub?.stripe_subscription_id === stripeSubscriptionId) {\n        // Already processed - return existing data\n        reply.send({\n          success: true,\n          pro_status: 'active_pro',\n          subscriptionStartAt: existingSub.subscription_start_at,\n        })\n        return\n      }\n\n      const subscription =\n        await stripe.subscriptions.retrieve(stripeSubscriptionId)\n\n      if (!subscription.items.data[0]?.current_period_start) {\n        throw new Error('Subscription missing current_period_start')\n      }\n\n      const subscriptionStartAt = new Date(\n        subscription.items.data[0].current_period_start * 1000,\n      )\n\n      const upserted = await SubscriptionsRepository.upsertActive(\n        userSub,\n        stripeCustomerId,\n        stripeSubscriptionId,\n        subscriptionStartAt,\n        null, // Clear subscription_end_at when reactivating\n      )\n\n      // End trial if applicable (idempotent - safe to call multiple times)\n      await TrialsRepository.completeTrial(userSub)\n\n      reply.send({\n        success: true,\n        pro_status: 'active_pro',\n        subscriptionStartAt: upserted.subscription_start_at,\n      })\n    } catch (error: any) {\n      fastify.log.error({ err: error }, 'Stripe confirm failed')\n      reply\n        .code(500)\n        .send({ success: false, error: error?.message || 'Server error' })\n    }\n  })\n\n  fastify.post('/billing/cancel', async (request, reply) => {\n    try {\n      const userSub = (requireAuth && (request as any).user?.sub) || undefined\n      if (!userSub) {\n        reply.code(401).send({ success: false, error: 'Unauthorized' })\n        return\n      }\n\n      // Check for paid subscription first\n      const sub = await SubscriptionsRepository.getByUserId(userSub)\n      if (sub && sub.stripe_subscription_id) {\n        // Schedule cancellation at period end instead of immediate cancellation\n        const updatedSubscription = await stripe.subscriptions.update(\n          sub.stripe_subscription_id,\n          { cancel_at_period_end: true },\n        )\n\n        // When cancel_at_period_end is true, cancel_at contains the period end date\n        const periodEnd = updatedSubscription.cancel_at\n          ? new Date(updatedSubscription.cancel_at * 1000)\n          : null\n\n        await SubscriptionsRepository.updateSubscriptionEndAt(\n          userSub,\n          periodEnd,\n        )\n\n        reply.send({ success: true })\n        return\n      }\n\n      // Check for active trial\n      const trial = await TrialsRepository.getByUserId(userSub)\n      if (trial && !trial.has_completed_trial) {\n        // Cancel the Stripe trial subscription if it exists\n        if (trial.stripe_subscription_id) {\n          await stripe.subscriptions.cancel(trial.stripe_subscription_id)\n        }\n\n        // Mark trial as completed\n        await TrialsRepository.completeTrial(userSub)\n\n        reply.send({ success: true })\n        return\n      }\n\n      // No active subscription or trial found\n      reply\n        .code(400)\n        .send({ success: false, error: 'No active subscription found' })\n    } catch (error: any) {\n      fastify.log.error(\n        { err: error },\n        'Stripe subscription cancellation failed',\n      )\n      reply\n        .code(500)\n        .send({ success: false, error: error?.message || 'Server error' })\n    }\n  })\n\n  fastify.get('/billing/status', async (request, reply) => {\n    try {\n      const userSub = (requireAuth && (request as any).user?.sub) || undefined\n      if (!userSub) {\n        reply.code(401).send({ success: false, error: 'Unauthorized' })\n        return\n      }\n\n      const sub = await SubscriptionsRepository.getByUserId(userSub)\n      const trial = await TrialsRepository.getByUserId(userSub)\n\n      // Calculate trial days from database (synced from Stripe via webhooks)\n      const trialStartAt = trial?.trial_start_at\n      const trialEndAt = trial?.trial_end_at\n      const trialDays =\n        trialStartAt && trialEndAt\n          ? Math.ceil(\n              (trialEndAt.getTime() - trialStartAt.getTime()) /\n                (24 * 60 * 60 * 1000),\n            )\n          : 14 // Fallback to 14 if dates not available\n\n      // If user has an active paid subscription, return that\n      if (sub) {\n        const trialBlock = {\n          trialDays,\n          trialStartAt: trialStartAt ? trialStartAt.toISOString() : null,\n          daysLeft: 0,\n          isTrialActive: false,\n          hasCompletedTrial: true,\n        }\n\n        const subscriptionEndAt = sub.subscription_end_at\n        const isScheduledForCancellation =\n          subscriptionEndAt !== null && subscriptionEndAt.getTime() > Date.now()\n\n        reply.send({\n          success: true,\n          pro_status: 'active_pro',\n          subscriptionStartAt: sub.subscription_start_at,\n          subscriptionEndAt: subscriptionEndAt\n            ? subscriptionEndAt.toISOString()\n            : null,\n          isScheduledForCancellation,\n          trial: trialBlock,\n        })\n        return\n      }\n\n      // Calculate trial status from database (synced from Stripe via webhooks)\n      const now = Date.now()\n      const isTrialActive =\n        !!trialEndAt &&\n        now < trialEndAt.getTime() &&\n        !trial?.has_completed_trial\n\n      let daysLeft = 0\n      if (trialEndAt && isTrialActive) {\n        const remainingMs = trialEndAt.getTime() - now\n        daysLeft = Math.max(0, Math.ceil(remainingMs / (24 * 60 * 60 * 1000)))\n      }\n\n      const trialBlock = {\n        trialDays,\n        trialStartAt: trialStartAt ? trialStartAt.toISOString() : null,\n        daysLeft,\n        isTrialActive,\n        hasCompletedTrial: !!trial?.has_completed_trial,\n      }\n\n      if (isTrialActive) {\n        reply.send({\n          success: true,\n          pro_status: 'free_trial',\n          trial: trialBlock,\n        })\n        return\n      }\n\n      reply.send({ success: true, pro_status: 'none', trial: trialBlock })\n    } catch (error: any) {\n      reply\n        .code(500)\n        .send({ success: false, error: error?.message || 'Server error' })\n    }\n  })\n\n  fastify.post('/billing/reactivate', async (request, reply) => {\n    try {\n      const userSub = (requireAuth && (request as any).user?.sub) || undefined\n      if (!userSub) {\n        reply.code(401).send({ success: false, error: 'Unauthorized' })\n        return\n      }\n\n      const sub = await SubscriptionsRepository.getByUserId(userSub)\n      if (!sub || !sub.stripe_subscription_id) {\n        reply\n          .code(400)\n          .send({ success: false, error: 'No active subscription found' })\n        return\n      }\n\n      await stripe.subscriptions.update(sub.stripe_subscription_id, {\n        cancel_at_period_end: false,\n      })\n\n      await SubscriptionsRepository.updateSubscriptionEndAt(userSub, null)\n\n      reply.send({ success: true })\n    } catch (error: any) {\n      fastify.log.error(\n        { err: error },\n        'Stripe subscription reactivation failed',\n      )\n      reply\n        .code(500)\n        .send({ success: false, error: error?.message || 'Server error' })\n    }\n  })\n}\n\n// Public routes that must be accessible without authentication\nexport const registerBillingPublicRoutes = async (fastify: FastifyInstance) => {\n  const APP_PROTOCOL = getEnv('APP_PROTOCOL') // e.g., ito-dev or ito\n\n  fastify.get('/billing/success', async (request, reply) => {\n    const { session_id } = request.query as { session_id?: string }\n    const deeplink = `${APP_PROTOCOL}://billing/success${\n      session_id ? `?session_id=${encodeURIComponent(session_id)}` : ''\n    }`\n    reply.type('text/html').send(renderDeepLinkHtml(deeplink))\n  })\n\n  fastify.get('/billing/cancel', async (_request, reply) => {\n    const deeplink = `${APP_PROTOCOL}://billing/cancel`\n    reply.type('text/html').send(renderDeepLinkHtml(deeplink))\n  })\n}\n\nexport default registerBillingRoutes\n"
  },
  {
    "path": "server/src/services/cloudWatchLogger.ts",
    "content": "import {\n  CloudWatchLogsClient,\n  CreateLogStreamCommand,\n  PutLogEventsCommand,\n  DescribeLogStreamsCommand,\n} from '@aws-sdk/client-cloudwatch-logs'\n\nexport interface CloudWatchLogEntry {\n  timestamp: number\n  message: string\n}\n\n/**\n * Shared CloudWatch logger for both client logs and timing analytics\n */\nexport class CloudWatchLogger {\n  private logsClient: CloudWatchLogsClient | null\n  private logGroupName: string | null\n  private logStreamName: string | null\n  private nextSequenceToken: string | undefined\n\n  constructor(logGroupName?: string | null, logStreamNameSuffix?: string) {\n    this.logGroupName = logGroupName || null\n    this.logsClient = logGroupName ? new CloudWatchLogsClient({}) : null\n    this.logStreamName = logGroupName\n      ? `${logStreamNameSuffix || 'logs'}-${new Date().toISOString().slice(0, 10)}`\n      : null\n  }\n\n  /**\n   * Ensure the CloudWatch log stream exists\n   */\n  async ensureStream(): Promise<void> {\n    if (!this.logsClient || !this.logGroupName || !this.logStreamName) return\n\n    try {\n      await this.logsClient.send(\n        new CreateLogStreamCommand({\n          logGroupName: this.logGroupName,\n          logStreamName: this.logStreamName,\n        }),\n      )\n    } catch (err: any) {\n      if (err?.name !== 'ResourceAlreadyExistsException') {\n        throw err\n      }\n    }\n\n    const desc = await this.logsClient.send(\n      new DescribeLogStreamsCommand({\n        logGroupName: this.logGroupName,\n        logStreamNamePrefix: this.logStreamName,\n        limit: 1,\n      }),\n    )\n\n    const stream = desc.logStreams?.[0]\n    this.nextSequenceToken = stream?.uploadSequenceToken\n  }\n\n  /**\n   * Send log entries to CloudWatch\n   * Returns true if CloudWatch is configured, false otherwise\n   */\n  async sendLogs(entries: CloudWatchLogEntry[]): Promise<boolean> {\n    // If no CloudWatch client configured, return false\n    if (!this.logsClient || !this.logGroupName || !this.logStreamName) {\n      return false\n    }\n\n    // Sort entries by timestamp\n    entries.sort((a, b) => a.timestamp - b.timestamp)\n\n    try {\n      const params = {\n        logGroupName: this.logGroupName,\n        logStreamName: this.logStreamName,\n        logEvents: entries,\n        sequenceToken: this.nextSequenceToken,\n      }\n\n      const res = await this.logsClient.send(new PutLogEventsCommand(params))\n      this.nextSequenceToken = res.nextSequenceToken\n      return true\n    } catch (err: any) {\n      // Retry once on sequence token error\n      if (err?.name === 'InvalidSequenceTokenException') {\n        await this.ensureStream()\n\n        const res = await this.logsClient.send(\n          new PutLogEventsCommand({\n            logGroupName: this.logGroupName,\n            logStreamName: this.logStreamName,\n            logEvents: entries,\n            sequenceToken: this.nextSequenceToken,\n          }),\n        )\n\n        this.nextSequenceToken = res.nextSequenceToken\n        return true\n      }\n\n      return false\n    }\n  }\n\n  /**\n   * Check if CloudWatch is configured\n   */\n  isConfigured(): boolean {\n    return !!(this.logsClient && this.logGroupName && this.logStreamName)\n  }\n}\n"
  },
  {
    "path": "server/src/services/errorInterceptor.ts",
    "content": "import type { Interceptor } from '@connectrpc/connect'\nimport { ConnectError, Code } from '@connectrpc/connect'\nimport { isAbortError, createAbortError } from '../utils/abortUtils.js'\n\nexport const errorInterceptor: Interceptor = next => async req => {\n  try {\n    return await next(req)\n  } catch (err) {\n    // If it's already a ConnectError, just re-throw it (logging happens in loggingInterceptor)\n    if (err instanceof ConnectError) {\n      throw err\n    }\n\n    // Log non-ConnectError errors\n    console.error('Unhandled error in RPC handler:', err)\n\n    // Check if this is a connection abort/reset error (client cancelled)\n    if (isAbortError(err)) {\n      console.log('Request cancelled by client (connection closed)')\n      throw createAbortError(err)\n    }\n\n    // Otherwise, wrap in a ConnectError\n    throw new ConnectError(\n      'Internal server error',\n      Code.Internal,\n      undefined,\n      undefined,\n      err,\n    )\n  }\n}\n"
  },
  {
    "path": "server/src/services/ito/audioUtils.ts",
    "content": "/**\n * Creates a 44-byte WAV header for raw PCM audio data.\n */\nexport function createWavHeader(\n  dataLength: number,\n  sampleRate: number,\n  channelCount: number,\n  bitDepth: number,\n): Buffer {\n  const header = Buffer.alloc(44)\n\n  // RIFF chunk descriptor\n  header.write('RIFF', 0)\n  header.writeUInt32LE(36 + dataLength, 4) // ChunkSize\n  header.write('WAVE', 8)\n\n  // \"fmt \" sub-chunk\n  header.write('fmt ', 12)\n  header.writeUInt32LE(16, 16) // Subchunk1Size (16 for PCM)\n  header.writeUInt16LE(1, 20) // AudioFormat (1 for PCM)\n  header.writeUInt16LE(channelCount, 22)\n  header.writeUInt32LE(sampleRate, 24)\n\n  const blockAlign = channelCount * (bitDepth / 8)\n  const byteRate = sampleRate * blockAlign\n\n  header.writeUInt32LE(byteRate, 28)\n  header.writeUInt16LE(blockAlign, 32)\n  header.writeUInt16LE(bitDepth, 34)\n\n  // \"data\" sub-chunk\n  header.write('data', 36)\n  header.writeUInt32LE(dataLength, 40)\n\n  return header\n}\n"
  },
  {
    "path": "server/src/services/ito/constants.ts",
    "content": "import { DEFAULT_ADVANCED_SETTINGS } from '../../constants/generated-defaults.js'\nimport { ItoMode } from '../../generated/ito_pb.js'\n\nexport const ITO_MODE_PROMPT: { [key in ItoMode]: string } = {\n  [ItoMode.TRANSCRIBE]: DEFAULT_ADVANCED_SETTINGS.transcriptionPrompt,\n  [ItoMode.EDIT]: DEFAULT_ADVANCED_SETTINGS.editingPrompt,\n}\n\nexport const ITO_MODE_SYSTEM_PROMPT: { [key in ItoMode]: string } = {\n  [ItoMode.TRANSCRIBE]: 'You are a helpful AI transcription assistant.',\n  [ItoMode.EDIT]: 'You are an AI assistant helping to edit documents.',\n}\n\nexport const DEFAULT_ADVANCED_SETTINGS_STRUCT = {\n  asrModel: DEFAULT_ADVANCED_SETTINGS.asrModel,\n  asrPrompt: DEFAULT_ADVANCED_SETTINGS.asrPrompt,\n  asrProvider: DEFAULT_ADVANCED_SETTINGS.asrProvider,\n  llmProvider: DEFAULT_ADVANCED_SETTINGS.llmProvider,\n  llmTemperature: DEFAULT_ADVANCED_SETTINGS.llmTemperature,\n  llmModel: DEFAULT_ADVANCED_SETTINGS.llmModel,\n  transcriptionPrompt: DEFAULT_ADVANCED_SETTINGS.transcriptionPrompt,\n  editingPrompt: DEFAULT_ADVANCED_SETTINGS.editingPrompt,\n  noSpeechThreshold: DEFAULT_ADVANCED_SETTINGS.noSpeechThreshold,\n}\n"
  },
  {
    "path": "server/src/services/ito/helpers.ts",
    "content": "import { HeaderValidator } from '../../validation/HeaderValidator.js'\nimport { ItoContext } from './types.js'\nimport { ITO_MODE_PROMPT } from './constants.js'\nimport { DEFAULT_ADVANCED_SETTINGS } from '../../constants/generated-defaults.js'\nimport { ItoMode } from '../../generated/ito_pb.js'\nimport {\n  END_APP_NAME_MARKER,\n  END_CONTEXT_MARKER,\n  END_USER_COMMAND_MARKER,\n  END_WINDOW_TITLE_MARKER,\n  START_APP_NAME_MARKER,\n  START_CONTEXT_MARKER,\n  START_USER_COMMAND_MARKER,\n  START_WINDOW_TITLE_MARKER,\n} from '../../constants/markers.js'\n\nexport function createUserPromptWithContext(\n  transcript: string,\n  context?: ItoContext,\n): string {\n  let contextPrompt = ''\n  if (context) {\n    if (context.windowTitle) {\n      contextPrompt += `\\n${START_WINDOW_TITLE_MARKER}\\n${context.windowTitle}\\n${END_WINDOW_TITLE_MARKER}`\n    }\n    if (context.appName) {\n      contextPrompt += `\\n${START_APP_NAME_MARKER}\\n${context.appName}\\n${END_APP_NAME_MARKER}`\n    }\n  }\n  const userPrompt = `\n    ${contextPrompt}${context?.contextText ? '\\n' : ''}\n    ${START_CONTEXT_MARKER}\n    ${context?.contextText || ''}\n    ${END_CONTEXT_MARKER}\n    ${START_USER_COMMAND_MARKER}\n    ${transcript}\n    ${END_USER_COMMAND_MARKER}\n  `\n  return userPrompt\n}\n\nfunction validateAndTransformHeaderValue<T>(\n  headers: Headers,\n  headerName: string,\n  defaultValue: T,\n  validator: (value: T) => T,\n  logName: string,\n): T {\n  const headerValue = headers.get(headerName)\n  let valueToValidate = (headerValue || defaultValue) as T\n  if (typeof defaultValue === 'number') {\n    valueToValidate = Number(valueToValidate) as T\n  }\n  const validatedValue = validator(valueToValidate)\n  console.log(\n    `[Transcription] Using validated ${logName}: ${typeof validatedValue === 'string' ? validatedValue.slice(0, 50) + '...' : validatedValue} (source: ${headerValue ? 'header' : 'default'})`,\n  )\n  return validatedValue\n}\n\nexport function getAdvancedSettingsHeaders(headers: Headers) {\n  const asrModel = validateAndTransformHeaderValue(\n    headers,\n    'asr-model',\n    DEFAULT_ADVANCED_SETTINGS.asrModel,\n    HeaderValidator.validateAsrModel,\n    'ASR model',\n  )\n\n  const asrProvider = validateAndTransformHeaderValue(\n    headers,\n    'asr-provider',\n    DEFAULT_ADVANCED_SETTINGS.asrProvider,\n    HeaderValidator.validateAsrProvider,\n    'ASR Provider',\n  )\n\n  const asrPrompt = validateAndTransformHeaderValue(\n    headers,\n    'asr-prompt',\n    DEFAULT_ADVANCED_SETTINGS.asrPrompt,\n    HeaderValidator.validateAsrPrompt,\n    'ASR prompt',\n  )\n\n  const llmProvider = validateAndTransformHeaderValue(\n    headers,\n    'llm-provider',\n    DEFAULT_ADVANCED_SETTINGS.llmProvider,\n    HeaderValidator.validateLlmProvider,\n    'LLM Provider',\n  )\n\n  const llmModel = validateAndTransformHeaderValue(\n    headers,\n    'llm-model',\n    DEFAULT_ADVANCED_SETTINGS.llmModel,\n    HeaderValidator.validateLlmModel,\n    'LLM model',\n  )\n\n  const llmTemperature = validateAndTransformHeaderValue(\n    headers,\n    'llm-temperature',\n    DEFAULT_ADVANCED_SETTINGS.llmTemperature,\n    HeaderValidator.validateLlmTemperature,\n    'LLM temperature',\n  )\n\n  const transcriptionPrompt = validateAndTransformHeaderValue(\n    headers,\n    'transcription-prompt',\n    DEFAULT_ADVANCED_SETTINGS.transcriptionPrompt,\n    HeaderValidator.validateTranscriptionPrompt,\n    'Transcription prompt',\n  )\n\n  const editingPrompt = validateAndTransformHeaderValue(\n    headers,\n    'editing-prompt',\n    DEFAULT_ADVANCED_SETTINGS.editingPrompt,\n    HeaderValidator.validateEditingPrompt,\n    'Editing prompt',\n  )\n\n  const noSpeechThreshold = validateAndTransformHeaderValue(\n    headers,\n    'no-speech-threshold',\n    DEFAULT_ADVANCED_SETTINGS.noSpeechThreshold,\n    HeaderValidator.validateNoSpeechThreshold,\n    'No speech threshold',\n  )\n\n  return {\n    asrModel,\n    asrProvider,\n    asrPrompt,\n    llmProvider,\n    llmModel,\n    llmTemperature,\n    transcriptionPrompt,\n    editingPrompt,\n    noSpeechThreshold,\n  }\n}\n\nexport function getItoMode(input: unknown): ItoMode | undefined {\n  try {\n    const inputNumber = Number(input)\n    if (isNaN(inputNumber) || !Number.isFinite(inputNumber)) {\n      return undefined\n    }\n\n    return inputNumber as ItoMode\n  } catch (error) {\n    console.error('Error parsing Ito mode:', error)\n    return undefined\n  }\n}\n\nexport function detectItoMode(transcript: string): ItoMode {\n  const words = transcript.trim().split(/\\s+/)\n  const firstFiveWords = words.slice(0, 5).join(' ').toLowerCase()\n\n  return firstFiveWords.includes('hey ito') ? ItoMode.EDIT : ItoMode.TRANSCRIBE\n}\n\nexport function getPromptForMode(\n  mode: ItoMode,\n  advancedSettingsHeaders: ReturnType<typeof getAdvancedSettingsHeaders>,\n): string {\n  switch (mode) {\n    case ItoMode.EDIT:\n      return (\n        // TODO: Figure out how to version advanced settings such that we can overwrite user settings when a significant change is made\n        // advancedSettingsHeaders.editingPrompt || ITO_MODE_PROMPT[ItoMode.EDIT]\n        ITO_MODE_PROMPT[ItoMode.EDIT]\n      )\n    case ItoMode.TRANSCRIBE:\n      return (\n        advancedSettingsHeaders.transcriptionPrompt ||\n        ITO_MODE_PROMPT[ItoMode.TRANSCRIBE]\n      )\n    default:\n      return ITO_MODE_PROMPT[ItoMode.TRANSCRIBE]\n  }\n}\n"
  },
  {
    "path": "server/src/services/ito/itoService.ts",
    "content": "import type { ConnectRouter } from '@connectrpc/connect'\nimport {\n  AudioChunk,\n  ItoService as ItoServiceDesc,\n  Note,\n  NoteSchema,\n  Interaction,\n  InteractionSchema,\n  DictionaryItem,\n  DictionaryItemSchema,\n  AdvancedSettings,\n  AdvancedSettingsSchema,\n  LlmSettingsSchema,\n  TranscribeStreamRequest,\n} from '../../generated/ito_pb.js'\nimport { create } from '@bufbuild/protobuf'\nimport type { HandlerContext } from '@connectrpc/connect'\nimport { getStorageClient } from '../../clients/s3storageClient.js'\nimport { v4 as uuidv4 } from 'uuid'\nimport { createAudioKey } from '../../constants/storage.js'\nimport {\n  DictionaryRepository,\n  InteractionsRepository,\n  NotesRepository,\n  AdvancedSettingsRepository,\n} from '../../db/repo.js'\nimport {\n  Note as DbNote,\n  Interaction as DbInteraction,\n  DictionaryItem as DbDictionaryItem,\n  AdvancedSettings as DbAdvancedSettings,\n} from '../../db/models.js'\nimport { ConnectError, Code } from '@connectrpc/connect'\nimport { kUser } from '../../auth/userContext.js'\nimport { transcribeStreamV2Handler } from './transcribeStreamV2Handler.js'\nimport { transcribeStreamHandler } from './transcribeStreamHandler.js'\nimport { DEFAULT_ADVANCED_SETTINGS_STRUCT } from './constants.js'\n\nfunction dbToNotePb(dbNote: DbNote): Note {\n  return create(NoteSchema, {\n    id: dbNote.id,\n    userId: dbNote.user_id,\n    interactionId: dbNote.interaction_id ?? '',\n    content: dbNote.content,\n    createdAt: dbNote.created_at.toISOString(),\n    updatedAt: dbNote.updated_at.toISOString(),\n    deletedAt: dbNote.deleted_at?.toISOString() ?? '',\n  })\n}\n\nfunction dbToInteractionPb(\n  dbInteraction: DbInteraction,\n  rawAudio?: Buffer,\n): Interaction {\n  let rawAudioDb: Uint8Array | undefined\n  if (rawAudio) {\n    rawAudioDb = new Uint8Array(rawAudio)\n  } else if (dbInteraction.raw_audio) {\n    rawAudioDb = new Uint8Array(dbInteraction.raw_audio)\n  } else {\n    rawAudioDb = undefined\n  }\n\n  return create(InteractionSchema, {\n    id: dbInteraction.id,\n    userId: dbInteraction.user_id ?? '',\n    title: dbInteraction.title ?? '',\n    asrOutput: dbInteraction.asr_output\n      ? JSON.stringify(dbInteraction.asr_output)\n      : '',\n    llmOutput: dbInteraction.llm_output\n      ? JSON.stringify(dbInteraction.llm_output)\n      : '',\n    rawAudio: rawAudioDb,\n    rawAudioId: dbInteraction.raw_audio_id ?? '',\n    durationMs: dbInteraction.duration_ms ?? 0,\n    createdAt: dbInteraction.created_at.toISOString(),\n    updatedAt: dbInteraction.updated_at.toISOString(),\n    deletedAt: dbInteraction.deleted_at?.toISOString() ?? '',\n  })\n}\n\nfunction dbToDictionaryItemPb(\n  dbDictionaryItem: DbDictionaryItem,\n): DictionaryItem {\n  return create(DictionaryItemSchema, {\n    id: dbDictionaryItem.id,\n    userId: dbDictionaryItem.user_id,\n    word: dbDictionaryItem.word,\n    pronunciation: dbDictionaryItem.pronunciation ?? '',\n    createdAt: dbDictionaryItem.created_at.toISOString(),\n    updatedAt: dbDictionaryItem.updated_at.toISOString(),\n    deletedAt: dbDictionaryItem.deleted_at?.toISOString() ?? '',\n  })\n}\n\nfunction dbToAdvancedSettingsPb(\n  dbAdvancedSettings: DbAdvancedSettings,\n): AdvancedSettings {\n  return create(AdvancedSettingsSchema, {\n    id: dbAdvancedSettings.id,\n    userId: dbAdvancedSettings.user_id,\n    createdAt: dbAdvancedSettings.created_at.toISOString(),\n    updatedAt: dbAdvancedSettings.updated_at.toISOString(),\n    llm: create(LlmSettingsSchema, {\n      // Convert null to undefined so protobuf omits unset optional fields\n      asrModel: dbAdvancedSettings.llm.asr_model ?? undefined,\n      asrPrompt: dbAdvancedSettings.llm.asr_prompt ?? undefined,\n      asrProvider: dbAdvancedSettings.llm.asr_provider ?? undefined,\n      llmProvider: dbAdvancedSettings.llm.llm_provider ?? undefined,\n      llmTemperature: dbAdvancedSettings.llm.llm_temperature ?? undefined,\n      llmModel: dbAdvancedSettings.llm.llm_model ?? undefined,\n      transcriptionPrompt:\n        dbAdvancedSettings.llm.transcription_prompt ?? undefined,\n      editingPrompt: dbAdvancedSettings.llm.editing_prompt ?? undefined,\n      noSpeechThreshold:\n        dbAdvancedSettings.llm.no_speech_threshold ?? undefined,\n      lowQualityThreshold:\n        dbAdvancedSettings.llm.low_quality_threshold ?? undefined,\n    }),\n    default: DEFAULT_ADVANCED_SETTINGS_STRUCT,\n  })\n}\n\n// Export the service implementation as a function that takes a ConnectRouter\nexport default (router: ConnectRouter) => {\n  router.service(ItoServiceDesc, {\n    async transcribeStreamV2(\n      requests: AsyncIterable<TranscribeStreamRequest>,\n      context: HandlerContext,\n    ) {\n      return transcribeStreamV2Handler.process(requests, context)\n    },\n\n    /**\n     * @deprecated Legacy endpoint maintained for backwards compatibility.\n     * New clients should use transcribeStreamV2.\n     */\n    async transcribeStream(\n      requests: AsyncIterable<AudioChunk>,\n      context: HandlerContext,\n    ) {\n      return transcribeStreamHandler.process(requests, context)\n    },\n    async createNote(request, context: HandlerContext) {\n      const user = context.values.get(kUser)\n      const userId = user?.sub\n      if (!userId) {\n        throw new ConnectError('User not authenticated', Code.Unauthenticated)\n      }\n      const noteRequest = { ...request, userId }\n      const newNote = await NotesRepository.create(noteRequest)\n      return dbToNotePb(newNote)\n    },\n\n    async getNote(request) {\n      const note = await NotesRepository.findById(request.id)\n      if (!note) {\n        throw new ConnectError('Note not found', Code.NotFound)\n      }\n      return dbToNotePb(note)\n    },\n\n    async listNotes(request, context: HandlerContext) {\n      const user = context.values.get(kUser)\n      const userId = user?.sub\n      if (!userId) {\n        throw new ConnectError('User not authenticated', Code.Unauthenticated)\n      }\n      const since = request.sinceTimestamp\n        ? new Date(request.sinceTimestamp)\n        : undefined\n      const notes = await NotesRepository.findByUserId(userId, since)\n      return { notes: notes.map(dbToNotePb) }\n    },\n\n    async updateNote(request) {\n      const updatedNote = await NotesRepository.update(request)\n      if (!updatedNote) {\n        throw new ConnectError('Note not found', Code.NotFound)\n      }\n      return dbToNotePb(updatedNote)\n    },\n\n    async deleteNote(request) {\n      await NotesRepository.softDelete(request.id)\n      return {}\n    },\n\n    async createInteraction(request, context: HandlerContext) {\n      const user = context.values.get(kUser)\n      const userId = user?.sub\n      if (!userId) {\n        throw new ConnectError('User not authenticated', Code.Unauthenticated)\n      }\n\n      let rawAudioId: string | undefined\n\n      // If raw audio is provided, upload to S3\n      if (request.rawAudio && request.rawAudio.length > 0) {\n        try {\n          const storageClient = getStorageClient()\n          rawAudioId = uuidv4()\n          const audioKey = createAudioKey(userId, rawAudioId)\n\n          // Upload audio to S3\n          await storageClient.uploadObject(\n            audioKey,\n            Buffer.from(request.rawAudio),\n            undefined, // ContentType\n            {\n              userId,\n              interactionId: request.id,\n              timestamp: new Date().toISOString(),\n            },\n          )\n\n          // Create interaction with UUID reference instead of blob\n          const interactionRequest = {\n            ...request,\n            userId,\n            rawAudioId,\n            rawAudio: undefined, // Don't store the blob in DB\n          }\n          const newInteraction =\n            await InteractionsRepository.create(interactionRequest)\n          return dbToInteractionPb(newInteraction)\n        } catch (error) {\n          console.error('Failed to upload audio to S3:', error)\n\n          throw new ConnectError(\n            'Failed to store interaction audio',\n            Code.Internal,\n          )\n        }\n      } else {\n        // No audio provided\n        const interactionRequest = { ...request, userId }\n        const newInteraction =\n          await InteractionsRepository.create(interactionRequest)\n        return dbToInteractionPb(newInteraction)\n      }\n    },\n\n    async getInteraction(request) {\n      const interaction = await InteractionsRepository.findById(request.id)\n      if (!interaction) {\n        throw new ConnectError('Interaction not found', Code.NotFound)\n      }\n\n      // If audio is stored in S3, fetch it\n      if (interaction.raw_audio_id && !interaction.raw_audio) {\n        try {\n          const storageClient = getStorageClient()\n          const userId = interaction.user_id || 'unknown'\n          const audioKey = createAudioKey(userId, interaction.raw_audio_id)\n\n          const { body } = await storageClient.getObject(audioKey)\n          if (body) {\n            // Convert stream to buffer\n            const chunks: Uint8Array[] = []\n            for await (const chunk of body) {\n              chunks.push(chunk as Uint8Array)\n            }\n            interaction.raw_audio = Buffer.concat(chunks)\n          }\n        } catch (error) {\n          console.error('Failed to fetch audio from S3:', error)\n          // Continue without audio if S3 fetch fails\n        }\n      }\n\n      return dbToInteractionPb(interaction)\n    },\n\n    async listInteractions(request, context: HandlerContext) {\n      const user = context.values.get(kUser)\n      const userId = user?.sub\n      if (!userId) {\n        throw new ConnectError('User not authenticated', Code.Unauthenticated)\n      }\n      const since = request.sinceTimestamp\n        ? new Date(request.sinceTimestamp)\n        : undefined\n      const interactions = await InteractionsRepository.findByUserId(\n        userId,\n        since,\n      )\n\n      // Create a map to store audio buffers by interaction ID\n      const rawAudioMap = new Map<string, Buffer>()\n\n      // Fetch all audio files from S3 in parallel\n      const storageClient = getStorageClient()\n      const audioFetchPromises = interactions\n        .filter(\n          interaction => interaction.raw_audio_id && !interaction.raw_audio,\n        )\n        .map(async interaction => {\n          try {\n            const audioKey = createAudioKey(\n              interaction.user_id || userId,\n              interaction.raw_audio_id!,\n            )\n            const { body } = await storageClient.getObject(audioKey)\n            if (body) {\n              // Convert stream to buffer\n              const chunks: Uint8Array[] = []\n              for await (const chunk of body) {\n                chunks.push(chunk as Uint8Array)\n              }\n              const buffer = Buffer.concat(chunks)\n              rawAudioMap.set(interaction.id, buffer)\n            }\n          } catch (error) {\n            console.error(\n              `Failed to fetch audio for interaction ${interaction.id}:`,\n              error,\n            )\n          }\n        })\n\n      // Wait for all audio fetches to complete\n      await Promise.all(audioFetchPromises)\n\n      return {\n        interactions: interactions.map(dbInteraction => {\n          // Use S3 audio if available\n          const audioBuffer = rawAudioMap.get(dbInteraction.id) || undefined\n          return dbToInteractionPb(dbInteraction, audioBuffer)\n        }),\n      }\n    },\n\n    async updateInteraction(request) {\n      const updatedInteraction = await InteractionsRepository.update(request)\n      if (!updatedInteraction) {\n        throw new ConnectError(\n          'Interaction not found or was deleted',\n          Code.NotFound,\n        )\n      }\n      return dbToInteractionPb(updatedInteraction)\n    },\n\n    async deleteInteraction(request) {\n      await InteractionsRepository.softDelete(request.id)\n      return {}\n    },\n\n    async createDictionaryItem(request, context: HandlerContext) {\n      const user = context.values.get(kUser)\n      const userId = user?.sub\n      if (!userId) {\n        throw new ConnectError('User not authenticated', Code.Unauthenticated)\n      }\n      const dictionaryRequest = { ...request, userId }\n      const newItem = await DictionaryRepository.create(dictionaryRequest)\n      return dbToDictionaryItemPb(newItem)\n    },\n\n    async listDictionaryItems(request, context: HandlerContext) {\n      const user = context.values.get(kUser)\n      const userId = user?.sub\n      if (!userId) {\n        throw new ConnectError('User not authenticated', Code.Unauthenticated)\n      }\n      const since = request.sinceTimestamp\n        ? new Date(request.sinceTimestamp)\n        : undefined\n      const items = await DictionaryRepository.findByUserId(userId, since)\n      return { items: items.map(dbToDictionaryItemPb) }\n    },\n\n    async updateDictionaryItem(request) {\n      const updatedItem = await DictionaryRepository.update(request)\n      if (!updatedItem) {\n        throw new ConnectError(\n          'Dictionary item not found or was deleted',\n          Code.NotFound,\n        )\n      }\n      return dbToDictionaryItemPb(updatedItem)\n    },\n\n    async deleteDictionaryItem(request) {\n      await DictionaryRepository.softDelete(request.id)\n      return {}\n    },\n\n    async deleteUserData(_request, context: HandlerContext) {\n      const user = context.values.get(kUser)\n      const userId = user?.sub\n      if (!userId) {\n        throw new ConnectError('User not authenticated', Code.Unauthenticated)\n      }\n\n      console.log(`Deleting all data for authenticated user: ${userId}`)\n\n      const storageClient = getStorageClient()\n      const audioPrefix = `raw-audio/${userId}/`\n\n      await Promise.all([\n        storageClient.hardDeletePrefix(audioPrefix),\n        NotesRepository.hardDeleteAllUserData(userId),\n        InteractionsRepository.hardDeleteAllUserData(userId),\n        DictionaryRepository.hardDeleteAllUserData(userId),\n        AdvancedSettingsRepository.hardDeleteByUserId(userId),\n      ])\n\n      console.log(`Successfully deleted all data for user: ${userId}`)\n      return {}\n    },\n\n    async getAdvancedSettings(_request, context: HandlerContext) {\n      const user = context.values.get(kUser)\n      const userId = user?.sub\n      if (!userId) {\n        throw new ConnectError('User not authenticated', Code.Unauthenticated)\n      }\n\n      const settings = await AdvancedSettingsRepository.findByUserId(userId)\n      if (!settings) {\n        // Return default settings if none exist\n        return create(AdvancedSettingsSchema, {\n          id: '',\n          userId: userId,\n          createdAt: new Date().toISOString(),\n          updatedAt: new Date().toISOString(),\n          llm: create(LlmSettingsSchema, {}),\n          default: DEFAULT_ADVANCED_SETTINGS_STRUCT,\n        })\n      }\n\n      return dbToAdvancedSettingsPb(settings)\n    },\n\n    async updateAdvancedSettings(request, context: HandlerContext) {\n      const user = context.values.get(kUser)\n      const userId = user?.sub\n      if (!userId) {\n        throw new ConnectError('User not authenticated', Code.Unauthenticated)\n      }\n\n      const updatedSettings = await AdvancedSettingsRepository.upsert(\n        userId,\n        request,\n      )\n      return dbToAdvancedSettingsPb(updatedSettings)\n    },\n  })\n}\n"
  },
  {
    "path": "server/src/services/ito/timingService.ts",
    "content": "import type { ConnectRouter } from '@connectrpc/connect'\nimport {\n  TimingService as TimingServiceDesc,\n  SubmitTimingReportsRequest,\n  SubmitTimingReportsResponse,\n  SubmitTimingReportsResponseSchema,\n} from '../../generated/ito_pb.js'\nimport { create } from '@bufbuild/protobuf'\nimport type { HandlerContext } from '@connectrpc/connect'\nimport { kUser } from '../../auth/userContext.js'\nimport { S3StorageClient } from '../../clients/s3storageClient.js'\n\n// Configuration for S3\nconst TIMING_BUCKET = process.env.TIMING_BUCKET\n\n// Initialize storage client for timing bucket\nlet timingStorageClient: S3StorageClient | null = null\nif (TIMING_BUCKET) {\n  try {\n    timingStorageClient = new S3StorageClient(TIMING_BUCKET)\n  } catch (error) {\n    console.error(\n      '[TimingService] Failed to initialize timing storage client:',\n      error,\n    )\n  }\n} else {\n  console.warn('[TimingService] TIMING_BUCKET environment variable not set')\n}\n\n// Export the service implementation as a function that takes a ConnectRouter\nexport default (router: ConnectRouter) => {\n  router.service(TimingServiceDesc, {\n    async submitTimingReports(\n      request: SubmitTimingReportsRequest,\n      context: HandlerContext,\n    ): Promise<SubmitTimingReportsResponse> {\n      if (!timingStorageClient) {\n        console.warn(\n          '[TimingService] Skipping timing report - no storage client configured',\n        )\n        return create(SubmitTimingReportsResponseSchema, {})\n      }\n\n      const user = context.values.get(kUser)\n      const userSub = user?.sub\n\n      // Write each timing report to S3 as a separate JSON file\n      const uploadPromises = request.reports.map(async report => {\n        const timingData = {\n          source: 'client',\n          interactionId: report.interactionId,\n          userId: userSub || report.userId,\n          platform: report.platform,\n          appVersion: report.appVersion,\n          hostname: report.hostname,\n          architecture: report.architecture,\n          timestamp: report.timestamp,\n          totalDurationMs: report.totalDurationMs,\n          events: report.events.map(event => ({\n            name: event.name,\n            startMs: event.startMs,\n            endMs: event.endMs,\n            durationMs: event.durationMs,\n          })),\n        }\n\n        const key = `client/${report.interactionId}/${Date.now()}.json`\n\n        try {\n          await timingStorageClient!.uploadObject(\n            key,\n            JSON.stringify(timingData),\n            'application/json',\n          )\n          console.log(`[TimingService] Uploaded client timing to S3: ${key}`)\n        } catch (error) {\n          console.error(\n            `[TimingService] Failed to upload timing to S3: ${key}`,\n            error,\n          )\n          // Don't throw - we don't want to fail the request if timing upload fails\n        }\n      })\n\n      // Wait for all uploads to complete\n      await Promise.allSettled(uploadPromises)\n\n      return create(SubmitTimingReportsResponseSchema, {})\n    },\n  })\n}\n"
  },
  {
    "path": "server/src/services/ito/transcribeStreamHandler.ts",
    "content": "/**\n * @deprecated This handler is for the legacy TranscribeStream (V1) endpoint.\n * New clients should use TranscribeStreamV2 (transcribeStreamV2Handler.ts).\n * This implementation is maintained for backwards compatibility with older app versions.\n *\n * Key differences from V2:\n * - Uses gRPC request headers for configuration instead of in-stream StreamConfig messages\n * - Accepts AudioChunk stream instead of TranscribeStreamRequest stream\n * - Does not support progressive config merging or mode grace period\n */\n\nimport { create } from '@bufbuild/protobuf'\nimport { ConnectError } from '@connectrpc/connect'\nimport type { HandlerContext } from '@connectrpc/connect'\nimport {\n  AudioChunk,\n  ItoMode,\n  TranscriptionResponseSchema,\n} from '../../generated/ito_pb.js'\nimport { getAsrProvider, getLlmProvider } from '../../clients/providerUtils.js'\nimport { enhancePcm16 } from '../../utils/audio.js'\nimport { errorToProtobuf } from '../../clients/errors.js'\nimport {\n  createUserPromptWithContext,\n  detectItoMode,\n  getAdvancedSettingsHeaders,\n  getItoMode,\n  getPromptForMode,\n} from './helpers.js'\nimport { ITO_MODE_SYSTEM_PROMPT } from './constants.js'\nimport type { ItoContext } from './types.js'\nimport { createWavHeader } from './audioUtils.js'\nimport { HeaderValidator } from '../../validation/HeaderValidator.js'\n\n/**\n * Legacy handler for TranscribeStream V1 endpoint.\n * @deprecated Maintained for backwards compatibility only.\n */\nexport class TranscribeStreamHandler {\n  async process(requests: AsyncIterable<AudioChunk>, context: HandlerContext) {\n    const startTime = Date.now()\n    const audioChunks: Uint8Array[] = []\n\n    console.log(\n      `📩 [${new Date().toISOString()}] Starting transcription stream (V1 - DEPRECATED)`,\n    )\n\n    // Process each audio chunk from the stream\n    for await (const chunk of requests) {\n      audioChunks.push(chunk.audioData)\n    }\n\n    console.log(\n      `📊 [${new Date().toISOString()}] Processed ${audioChunks.length} audio chunks`,\n    )\n\n    // Concatenate all audio chunks\n    const totalLength = audioChunks.reduce(\n      (sum, chunk) => sum + chunk.length,\n      0,\n    )\n    const fullAudio = new Uint8Array(totalLength)\n    let offset = 0\n    for (const chunk of audioChunks) {\n      fullAudio.set(chunk, offset)\n      offset += chunk.length\n    }\n\n    console.log(\n      `🔧 [${new Date().toISOString()}] Concatenated audio: ${totalLength} bytes`,\n    )\n\n    // Extract settings headers first so they're available in catch block\n    const advancedSettingsHeaders = getAdvancedSettingsHeaders(\n      context.requestHeader,\n    )\n\n    try {\n      // 1. Set audio properties to match the new capture settings.\n      const sampleRate = 16000 // Correct sample rate\n      const bitDepth = 16\n      const channels = 1 // Mono\n\n      // 2. Enhance the PCM and create the header with the correct properties.\n      const enhancedPcm = enhancePcm16(Buffer.from(fullAudio), sampleRate)\n      const wavHeader = createWavHeader(\n        enhancedPcm.length,\n        sampleRate,\n        channels,\n        bitDepth,\n      )\n      const fullAudioWAV = Buffer.concat([wavHeader, enhancedPcm])\n\n      // 3. Extract and validate vocabulary from gRPC metadata\n      const vocabularyHeader = context.requestHeader.get('vocabulary')\n      const vocabulary = vocabularyHeader\n        ? HeaderValidator.validateVocabulary(vocabularyHeader)\n        : []\n\n      // 4. Send the corrected WAV file using the selected ASR provider\n      const asrProvider = getAsrProvider(advancedSettingsHeaders.asrProvider)\n      let transcript = await asrProvider.transcribeAudio(fullAudioWAV, {\n        fileType: 'wav',\n        asrModel: advancedSettingsHeaders.asrModel,\n        noSpeechThreshold: advancedSettingsHeaders.noSpeechThreshold,\n        vocabulary,\n      })\n      console.log(\n        `📝 [${new Date().toISOString()}] Received transcript: \"${transcript}\"`,\n      )\n\n      const windowTitle = context.requestHeader.get('window-title') || ''\n      const appName = context.requestHeader.get('app-name') || ''\n      const mode = getItoMode(context.requestHeader.get('mode'))\n\n      // Decode context text if it was base64 encoded due to Unicode characters\n      const rawContextText = context.requestHeader.get('context-text') || ''\n      const contextText = rawContextText.startsWith('base64:')\n        ? Buffer.from(rawContextText.substring(7), 'base64').toString('utf8')\n        : rawContextText\n\n      const windowContext: ItoContext = { windowTitle, appName, contextText }\n\n      const detectedMode = mode || detectItoMode(transcript)\n      const userPromptPrefix = getPromptForMode(\n        detectedMode,\n        advancedSettingsHeaders,\n      )\n      const userPrompt = createUserPromptWithContext(transcript, windowContext)\n\n      console.log(\n        `[${new Date().toISOString()}] Detected mode: ${detectedMode}, adjusting transcript`,\n      )\n\n      if (detectedMode === ItoMode.EDIT) {\n        const llmProvider = getLlmProvider(advancedSettingsHeaders.llmProvider)\n        transcript = await llmProvider.adjustTranscript(\n          userPromptPrefix + '\\n' + userPrompt,\n          {\n            temperature: advancedSettingsHeaders.llmTemperature,\n            model: advancedSettingsHeaders.llmModel,\n            prompt: ITO_MODE_SYSTEM_PROMPT[detectedMode],\n          },\n        )\n        console.log(\n          `📝 [${new Date().toISOString()}] Adjusted transcript: \"${transcript}\"`,\n        )\n      }\n\n      const duration = Date.now() - startTime\n      console.log(\n        `✅ [${new Date().toISOString()}] Transcription completed in ${duration}ms`,\n      )\n\n      return create(TranscriptionResponseSchema, {\n        transcript,\n      })\n    } catch (error: any) {\n      // Re-throw ConnectError validation errors - these should bubble up\n      if (error instanceof ConnectError) {\n        throw error\n      }\n\n      console.error('Failed to process transcription via GroqClient:', error)\n\n      // Return structured error response\n      return create(TranscriptionResponseSchema, {\n        transcript: '',\n        error: errorToProtobuf(\n          error,\n          advancedSettingsHeaders.asrProvider as any,\n        ),\n      })\n    }\n  }\n}\n\nexport const transcribeStreamHandler = new TranscribeStreamHandler()\n"
  },
  {
    "path": "server/src/services/ito/transcribeStreamV2Handler.ts",
    "content": "import { create } from '@bufbuild/protobuf'\nimport { ConnectError, Code } from '@connectrpc/connect'\nimport type { HandlerContext } from '@connectrpc/connect'\nimport {\n  ContextInfo,\n  ItoMode,\n  StreamConfig,\n  StreamConfigSchema,\n  TranscribeStreamRequest,\n  TranscriptionResponseSchema,\n} from '../../generated/ito_pb.js'\nimport { getAsrProvider, getLlmProvider } from '../../clients/providerUtils.js'\nimport { DEFAULT_ADVANCED_SETTINGS } from '../../constants/generated-defaults.js'\nimport { errorToProtobuf } from '../../clients/errors.js'\nimport {\n  createUserPromptWithContext,\n  detectItoMode,\n  getPromptForMode,\n} from './helpers.js'\nimport { ITO_MODE_SYSTEM_PROMPT } from './constants.js'\nimport type { ItoContext } from './types.js'\nimport { isAbortError, createAbortError } from '../../utils/abortUtils.js'\nimport {\n  concatenateAudioChunks,\n  prepareAudioForTranscription,\n} from '../../utils/audioProcessing.js'\nimport {\n  serverTimingCollector,\n  ServerTimingEventName,\n} from '../timing/ServerTimingCollector.js'\nimport { kUser } from '../../auth/userContext.js'\n\nexport class TranscribeStreamV2Handler {\n  private readonly MODE_CHANGE_GRACE_PERIOD_MS = 100\n\n  async process(\n    requests: AsyncIterable<TranscribeStreamRequest>,\n    context?: HandlerContext,\n  ) {\n    const startTime = Date.now()\n\n    console.log(`📩 [${new Date().toISOString()}] Starting TranscribeStreamV2`)\n\n    // Collect stream data\n    const {\n      audioChunks,\n      mergedConfig: initialConfig,\n      lastModeChangeTimestamp,\n      previousMode,\n    } = await this.collectStreamData(requests)\n\n    const streamEndTime = Date.now()\n\n    // Extract interaction ID and user ID for timing\n    const interactionId = initialConfig?.interactionId\n    const userId = context?.values.get(kUser)?.sub\n\n    // Initialize timing collection\n    serverTimingCollector.startInteraction(interactionId, userId)\n    serverTimingCollector.startTiming(\n      ServerTimingEventName.TOTAL_PROCESSING,\n      interactionId,\n    )\n\n    // Check if client cancelled the stream\n    if (context?.signal.aborted) {\n      serverTimingCollector.clearInteraction(interactionId)\n\n      console.log(\n        `🚫 [${new Date().toISOString()}] Stream cancelled by client, aborting processing`,\n      )\n      throw new ConnectError('Stream cancelled by client', Code.Canceled)\n    }\n\n    // Apply mode grace period\n    const mergedConfig = this.applyModeGracePeriod(\n      initialConfig,\n      lastModeChangeTimestamp,\n      previousMode,\n      streamEndTime,\n    )\n\n    console.log(\n      `📊 [${new Date().toISOString()}] Processed ${audioChunks.length} audio chunks`,\n    )\n\n    // Concatenate and prepare audio\n    const fullAudio = concatenateAudioChunks(audioChunks)\n\n    try {\n      // Time audio processing\n      const fullAudioWAV = interactionId\n        ? await serverTimingCollector.timeAsync(\n            ServerTimingEventName.AUDIO_PROCESSING,\n            () => prepareAudioForTranscription(fullAudio),\n            interactionId,\n          )\n        : prepareAudioForTranscription(fullAudio)\n\n      // Extract configuration\n      const asrConfig = this.extractAsrConfig(mergedConfig)\n\n      // Time transcription\n      let transcript = await serverTimingCollector.timeAsync(\n        ServerTimingEventName.ASR_TRANSCRIPTION,\n        () => this.transcribeAudioData(fullAudioWAV, asrConfig, context),\n        interactionId,\n      )\n\n      // Prepare context and settings\n      const windowContext: ItoContext = {\n        windowTitle: mergedConfig.context?.windowTitle || '',\n        appName: mergedConfig.context?.appName || '',\n        contextText: mergedConfig.context?.contextText || '',\n      }\n\n      const mode = mergedConfig.context?.mode ?? detectItoMode(transcript)\n\n      const advancedSettings = this.prepareAdvancedSettings(\n        mergedConfig,\n        asrConfig.asrModel,\n        asrConfig.asrProvider,\n        asrConfig.noSpeechThreshold,\n      )\n\n      // Time transcript adjustment (only happens in EDIT mode)\n      // transcript = await serverTimingCollector.timeAsync(\n      //   ServerTimingEventName.LLM_ADJUSTMENT,\n      //   () =>\n      //     this.adjustTranscriptForMode(\n      //       transcript,\n      //       mode,\n      //       windowContext,\n      //       advancedSettings,\n      //     ),\n      //   interactionId,\n      // )\n      transcript = await this.adjustTranscriptForMode(\n        transcript,\n        mode,\n        windowContext,\n        advancedSettings,\n      )\n\n      const duration = Date.now() - startTime\n\n      // Finalize timing\n      serverTimingCollector.endTiming(\n        ServerTimingEventName.TOTAL_PROCESSING,\n        interactionId,\n      )\n      serverTimingCollector.finalizeInteraction(interactionId)\n\n      console.log(\n        `✅ [${new Date().toISOString()}] TranscribeStreamV2 completed in ${duration}ms`,\n      )\n\n      return create(TranscriptionResponseSchema, {\n        transcript,\n      })\n    } catch (error: any) {\n      // Clear timing on error\n      if (interactionId) {\n        serverTimingCollector.clearInteraction(interactionId)\n      }\n\n      if (error instanceof ConnectError) {\n        throw error\n      }\n\n      console.error('Failed to process TranscribeStreamV2:', error)\n\n      return create(TranscriptionResponseSchema, {\n        transcript: '',\n        error: errorToProtobuf(\n          error,\n          (mergedConfig.llmSettings?.asrProvider as any) ||\n            (DEFAULT_ADVANCED_SETTINGS.asrProvider as any),\n        ),\n      })\n    }\n  }\n\n  private async collectStreamData(\n    requests: AsyncIterable<TranscribeStreamRequest>,\n  ): Promise<{\n    audioChunks: Uint8Array[]\n    mergedConfig: StreamConfig\n    lastModeChangeTimestamp: number | null\n    previousMode: ItoMode | undefined\n  }> {\n    const audioChunks: Uint8Array[] = []\n    let mergedConfig: StreamConfig = create(StreamConfigSchema, {\n      context: undefined,\n      llmSettings: undefined,\n      vocabulary: [],\n    })\n    let lastModeChangeTimestamp: number | null = null\n    let previousMode: ItoMode | undefined = undefined\n\n    try {\n      for await (const request of requests) {\n        if (request.payload.case === 'audioData') {\n          audioChunks.push(request.payload.value)\n        } else if (request.payload.case === 'config') {\n          const currentMode = mergedConfig.context?.mode\n          mergedConfig = this.mergeStreamConfigs(\n            mergedConfig,\n            request.payload.value,\n          )\n\n          console.log(\n            `🔧 [${new Date().toISOString()}] Received config update:`,\n            JSON.stringify(mergedConfig, null, 2),\n          )\n\n          const newMode = mergedConfig.context?.mode\n          if (newMode !== undefined && newMode !== currentMode) {\n            previousMode = currentMode\n            lastModeChangeTimestamp = Date.now()\n            console.log(\n              `🔧 [${new Date().toISOString()}] Mode changed from ${currentMode} to: ${newMode}`,\n            )\n          }\n        }\n      }\n    } catch (err) {\n      if (isAbortError(err)) {\n        console.log(\n          `🚫 [${new Date().toISOString()}] Stream reading interrupted (client cancelled)`,\n        )\n        throw createAbortError(err, 'Stream cancelled by client')\n      }\n\n      throw err\n    }\n\n    return { audioChunks, mergedConfig, lastModeChangeTimestamp, previousMode }\n  }\n\n  private applyModeGracePeriod(\n    mergedConfig: StreamConfig,\n    lastModeChangeTimestamp: number | null,\n    previousMode: ItoMode | undefined,\n    streamEndTime: number,\n  ): StreamConfig {\n    // If there was a mode change and it happened within the grace period,\n    // revert to the previous mode (or undefined if no previous mode)\n    if (lastModeChangeTimestamp !== null) {\n      const timeSinceLastChange = streamEndTime - lastModeChangeTimestamp\n\n      if (timeSinceLastChange <= this.MODE_CHANGE_GRACE_PERIOD_MS) {\n        const currentMode = mergedConfig.context?.mode\n        console.log(\n          `⏱️ [${new Date().toISOString()}] Last mode change (${timeSinceLastChange}ms ago) within grace period (${this.MODE_CHANGE_GRACE_PERIOD_MS}ms) - reverting from ${currentMode} to ${previousMode}`,\n        )\n\n        if (mergedConfig.context) {\n          return {\n            ...mergedConfig,\n            context: {\n              ...mergedConfig.context,\n              mode: previousMode,\n            },\n          }\n        }\n      }\n    }\n\n    return mergedConfig\n  }\n\n  private extractAsrConfig(mergedConfig: StreamConfig) {\n    return {\n      asrModel: this.resolveOrDefault(\n        mergedConfig.llmSettings?.asrModel,\n        DEFAULT_ADVANCED_SETTINGS.asrModel,\n      ),\n      asrProvider: this.resolveOrDefault(\n        mergedConfig.llmSettings?.asrProvider,\n        DEFAULT_ADVANCED_SETTINGS.asrProvider,\n      ),\n      noSpeechThreshold: this.resolveOrDefault(\n        mergedConfig.llmSettings?.noSpeechThreshold,\n        DEFAULT_ADVANCED_SETTINGS.noSpeechThreshold,\n      ),\n      vocabulary: mergedConfig.vocabulary,\n    }\n  }\n\n  /**\n   * Resolves a value to its default if it's undefined, null, or empty.\n   * This provides a defensive fallback for optional protobuf fields.\n   */\n  private resolveOrDefault<T extends string | number>(\n    value: T | undefined,\n    defaultValue: T,\n  ): T {\n    if (value === undefined || value === '' || value === null) {\n      return defaultValue\n    }\n    return value\n  }\n\n  private prepareAdvancedSettings(\n    mergedConfig: StreamConfig,\n    asrModel: string,\n    asrProvider: string,\n    noSpeechThreshold: number,\n  ) {\n    return {\n      asrModel: this.resolveOrDefault(asrModel, DEFAULT_ADVANCED_SETTINGS.asrModel),\n      asrProvider: this.resolveOrDefault(\n        asrProvider,\n        DEFAULT_ADVANCED_SETTINGS.asrProvider,\n      ),\n      asrPrompt: this.resolveOrDefault(\n        mergedConfig.llmSettings?.asrPrompt,\n        DEFAULT_ADVANCED_SETTINGS.asrPrompt,\n      ),\n      llmProvider: this.resolveOrDefault(\n        mergedConfig.llmSettings?.llmProvider,\n        DEFAULT_ADVANCED_SETTINGS.llmProvider,\n      ),\n      llmModel: this.resolveOrDefault(\n        mergedConfig.llmSettings?.llmModel,\n        DEFAULT_ADVANCED_SETTINGS.llmModel,\n      ),\n      llmTemperature: this.resolveOrDefault(\n        mergedConfig.llmSettings?.llmTemperature,\n        DEFAULT_ADVANCED_SETTINGS.llmTemperature,\n      ),\n      transcriptionPrompt: this.resolveOrDefault(\n        mergedConfig.llmSettings?.transcriptionPrompt,\n        DEFAULT_ADVANCED_SETTINGS.transcriptionPrompt,\n      ),\n      editingPrompt: this.resolveOrDefault(\n        mergedConfig.llmSettings?.editingPrompt,\n        DEFAULT_ADVANCED_SETTINGS.editingPrompt,\n      ),\n      noSpeechThreshold: this.resolveOrDefault(\n        noSpeechThreshold,\n        DEFAULT_ADVANCED_SETTINGS.noSpeechThreshold,\n      ),\n    }\n  }\n\n  private async transcribeAudioData(\n    audioWav: Buffer,\n    asrConfig: ReturnType<typeof this.extractAsrConfig>,\n    context?: HandlerContext,\n  ): Promise<string> {\n    if (context?.signal.aborted) {\n      console.log(\n        `🚫 [${new Date().toISOString()}] Stream cancelled before ASR call, skipping transcription`,\n      )\n      throw new ConnectError('Stream cancelled by client', Code.Canceled)\n    }\n\n    const asrClient = getAsrProvider(asrConfig.asrProvider)\n    const transcript = await asrClient.transcribeAudio(audioWav, {\n      fileType: 'wav',\n      asrModel: asrConfig.asrModel,\n      noSpeechThreshold: asrConfig.noSpeechThreshold,\n      vocabulary: asrConfig.vocabulary,\n    })\n\n    console.log(\n      `📝 [${new Date().toISOString()}] Received transcript: \"${transcript}\"`,\n    )\n\n    return transcript\n  }\n\n  private async adjustTranscriptForMode(\n    transcript: string,\n    mode: ItoMode,\n    windowContext: ItoContext,\n    advancedSettings: ReturnType<typeof this.prepareAdvancedSettings>,\n  ): Promise<string> {\n    console.log(\n      `[${new Date().toISOString()}] Detected mode: ${mode}, adjusting transcript`,\n    )\n\n    if (mode !== ItoMode.EDIT) {\n      return transcript\n    }\n\n    const userPromptPrefix = getPromptForMode(mode, advancedSettings)\n    const userPrompt = createUserPromptWithContext(transcript, windowContext)\n    const llmProvider = getLlmProvider(advancedSettings.llmProvider)\n\n    const adjustedTranscript = await serverTimingCollector.timeAsync(\n      ServerTimingEventName.LLM_ADJUSTMENT,\n      () =>\n        llmProvider.adjustTranscript(userPromptPrefix + '\\n' + userPrompt, {\n          temperature: advancedSettings.llmTemperature,\n          model: advancedSettings.llmModel,\n          prompt: ITO_MODE_SYSTEM_PROMPT[mode],\n        }),\n    )\n\n    console.log(\n      `📝 [${new Date().toISOString()}] Adjusted transcript: \"${adjustedTranscript}\"`,\n    )\n\n    return adjustedTranscript\n  }\n\n  private mergeStreamConfigs(\n    base: StreamConfig,\n    update: StreamConfig,\n  ): StreamConfig {\n    const mergeContext = (\n      baseCtx: ContextInfo | undefined,\n      updateCtx: ContextInfo | undefined,\n    ): ContextInfo | undefined => {\n      if (!updateCtx) return baseCtx\n      if (!baseCtx) return updateCtx\n\n      return {\n        ...baseCtx,\n        mode: updateCtx.mode !== undefined ? updateCtx.mode : baseCtx.mode,\n        windowTitle:\n          updateCtx.windowTitle !== ''\n            ? updateCtx.windowTitle\n            : baseCtx.windowTitle,\n        appName: updateCtx.appName !== '' ? updateCtx.appName : baseCtx.appName,\n        contextText:\n          updateCtx.contextText !== ''\n            ? updateCtx.contextText\n            : baseCtx.contextText,\n      }\n    }\n\n    return {\n      ...base,\n      context: mergeContext(base.context, update.context),\n      llmSettings: update.llmSettings\n        ? { ...base.llmSettings, ...update.llmSettings }\n        : base.llmSettings,\n      vocabulary:\n        update.vocabulary.length > 0 ? update.vocabulary : base.vocabulary,\n      interactionId: update.interactionId || base.interactionId,\n    }\n  }\n}\n\nexport const transcribeStreamV2Handler = new TranscribeStreamV2Handler()\n"
  },
  {
    "path": "server/src/services/ito/types.ts",
    "content": "export type ItoContext = {\n  windowTitle: string\n  appName: string\n  contextText: string\n}\n"
  },
  {
    "path": "server/src/services/logging.test.ts",
    "content": "import { describe, it, expect, mock, beforeEach, afterEach } from 'bun:test'\nimport {\n  type AnyObject,\n  createTestApp,\n  addAuthHook,\n} from './__tests__/helpers.js'\n\n// Mock the AWS SDK module used by logging.ts before importing the module under test\nconst awsMockState: {\n  putShouldThrowInvalidTokenOnce: boolean\n  sends: AnyObject[]\n} = {\n  putShouldThrowInvalidTokenOnce: false,\n  sends: [],\n}\n\nmock.module('@aws-sdk/client-cloudwatch-logs', () => {\n  class CreateLogStreamCommand {\n    input: AnyObject\n    constructor(input: AnyObject) {\n      this.input = input\n    }\n  }\n  class DescribeLogStreamsCommand {\n    input: AnyObject\n    constructor(input: AnyObject) {\n      this.input = input\n    }\n  }\n  class PutLogEventsCommand {\n    input: AnyObject\n    constructor(input: AnyObject) {\n      this.input = input\n    }\n  }\n  class CloudWatchLogsClient {\n    async send(command: any): Promise<any> {\n      awsMockState.sends.push({\n        type: command.constructor.name,\n        input: command.input,\n      })\n      if (command instanceof DescribeLogStreamsCommand) {\n        return { logStreams: [{ uploadSequenceToken: 'token-from-describe' }] }\n      }\n      if (command instanceof PutLogEventsCommand) {\n        if (awsMockState.putShouldThrowInvalidTokenOnce) {\n          awsMockState.putShouldThrowInvalidTokenOnce = false\n          const err = new Error('Invalid sequence token') as any\n          err.name = 'InvalidSequenceTokenException'\n          throw err\n        }\n        return { nextSequenceToken: 'next-token' }\n      }\n      // CreateLogStreamCommand success result\n      return {}\n    }\n  }\n  return {\n    CloudWatchLogsClient,\n    CreateLogStreamCommand,\n    PutLogEventsCommand,\n    DescribeLogStreamsCommand,\n    __awsMockState: awsMockState,\n  }\n})\n\n// Import after mocks are in place\nimport { registerLoggingRoutes } from './logging.js'\n\nconst originalStdoutWrite = process.stdout.write\n\ndescribe('registerLoggingRoutes', () => {\n  beforeEach(() => {\n    // reset mocks\n    const { __awsMockState } =\n      require('@aws-sdk/client-cloudwatch-logs') as AnyObject\n    __awsMockState.putShouldThrowInvalidTokenOnce = false\n    __awsMockState.sends.length = 0\n  })\n\n  afterEach(() => {\n    process.stdout.write = originalStdoutWrite\n  })\n\n  it('returns 400 for invalid body', async () => {\n    const app = createTestApp()\n    await registerLoggingRoutes(app, {\n      requireAuth: false,\n      showClientLogs: true,\n      clientLogGroupName: null,\n    })\n\n    const res = await app.inject({\n      method: 'POST',\n      url: '/logs',\n      payload: { bad: true } as any,\n    })\n\n    expect(res.statusCode).toBe(400)\n    await app.close()\n  })\n\n  it('falls back to stdout when no log group configured', async () => {\n    const writes: string[] = []\n    process.stdout.write = mock((chunk: string | Uint8Array) => {\n      writes.push(chunk.toString())\n      return true\n    }) as any\n\n    const app = createTestApp()\n    // Add a hook to simulate authenticated user when requireAuth is true; here false so ignored\n    await registerLoggingRoutes(app, {\n      requireAuth: false,\n      showClientLogs: true,\n      clientLogGroupName: null,\n    })\n\n    const res = await app.inject({\n      method: 'POST',\n      url: '/logs',\n      payload: {\n        events: [\n          { ts: Date.now(), level: 'info', message: 'hello', source: 'test' },\n          {\n            ts: Date.now() + 1,\n            level: 'debug',\n            message: 'world',\n            fields: { a: 1 },\n          },\n        ],\n      },\n    })\n\n    expect(res.statusCode).toBe(204)\n    expect(writes.length).toBe(2)\n    // Ensure the logged JSON is valid and contains expected fields\n    for (const line of writes) {\n      const json = JSON.parse(line)\n      expect(json).toHaveProperty('message')\n      expect(json).toHaveProperty('level')\n      expect(json.source).toBeDefined()\n    }\n\n    await app.close()\n  })\n\n  it('initializes CloudWatch stream and sends events (happy path)', async () => {\n    const app = createTestApp()\n    await registerLoggingRoutes(app, {\n      requireAuth: true,\n      showClientLogs: true,\n      clientLogGroupName: 'my-log-group',\n    })\n\n    const { __awsMockState } =\n      require('@aws-sdk/client-cloudwatch-logs') as AnyObject\n\n    const earlyCalls = __awsMockState.sends.map((c: AnyObject) => c.type)\n    // ensureStream is called during registration\n    expect(earlyCalls).toEqual([\n      'CreateLogStreamCommand',\n      'DescribeLogStreamsCommand',\n    ])\n\n    // Add a hook to simulate user info when auth is required\n    addAuthHook(app)\n\n    const res = await app.inject({\n      method: 'POST',\n      url: '/logs',\n      payload: {\n        events: [\n          { ts: 2, level: 'info', message: 'b' },\n          { ts: 1, level: 'debug', message: 'a' },\n        ],\n      },\n    })\n\n    expect(res.statusCode).toBe(204)\n\n    // Verify a PutLogEvents was sent with sorted events and sequence token\n    const putCalls = __awsMockState.sends.filter(\n      (c: AnyObject) => c.type === 'PutLogEventsCommand',\n    )\n    expect(putCalls).toHaveLength(1)\n    const putInput = putCalls[0].input\n    expect(putInput.logGroupName).toBe('my-log-group')\n    expect(Array.isArray(putInput.logEvents)).toBe(true)\n    expect(putInput.logEvents.map((e: any) => e.timestamp)).toEqual([1, 2])\n    expect(putInput.sequenceToken).toBe('token-from-describe')\n\n    await app.close()\n  })\n\n  it('retries once on InvalidSequenceTokenException', async () => {\n    const app = createTestApp()\n    await registerLoggingRoutes(app, {\n      requireAuth: false,\n      showClientLogs: true,\n      clientLogGroupName: 'retry-group',\n    })\n\n    const { __awsMockState } =\n      require('@aws-sdk/client-cloudwatch-logs') as AnyObject\n    __awsMockState.putShouldThrowInvalidTokenOnce = true\n\n    const res = await app.inject({\n      method: 'POST',\n      url: '/logs',\n      payload: {\n        events: [\n          { ts: 5, level: 'info', message: 'x' },\n          { ts: 4, level: 'warn', message: 'y' },\n        ],\n      },\n    })\n\n    expect(res.statusCode).toBe(204)\n\n    const calls = __awsMockState.sends.map((c: AnyObject) => c.type)\n    // Registration ensureStream + first failing Put + ensureStream (create+describe) + second Put\n    expect(calls).toEqual([\n      'CreateLogStreamCommand',\n      'DescribeLogStreamsCommand',\n      'PutLogEventsCommand',\n      'CreateLogStreamCommand',\n      'DescribeLogStreamsCommand',\n      'PutLogEventsCommand',\n    ])\n\n    await app.close()\n  })\n\n  it('coerces unknown levels to info', async () => {\n    const writes: string[] = []\n    process.stdout.write = mock((chunk: string | Uint8Array) => {\n      writes.push(chunk.toString())\n      return true\n    }) as any\n\n    const app = createTestApp()\n    await registerLoggingRoutes(app, {\n      requireAuth: false,\n      showClientLogs: true,\n      clientLogGroupName: null,\n    })\n\n    const res = await app.inject({\n      method: 'POST',\n      url: '/logs',\n      payload: {\n        events: [\n          // cast as any to bypass compile-time union\n          { ts: Date.now(), level: 'silly', message: 'noise' } as any,\n        ],\n      },\n    })\n    expect(res.statusCode).toBe(204)\n    const json = JSON.parse(writes[0])\n    expect(json.level).toBe('info')\n    await app.close()\n  })\n})\n"
  },
  {
    "path": "server/src/services/logging.ts",
    "content": "import type { FastifyInstance } from 'fastify'\nimport { CloudWatchLogger } from './cloudWatchLogger.js'\n\ntype LogEvent = {\n  ts: number\n  level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'log'\n  message: string\n  fields?: Record<string, unknown>\n  interactionId?: string\n  traceId?: string\n  spanId?: string\n  appVersion?: string\n  platform?: string\n  source?: string\n}\n\nexport const registerLoggingRoutes = async (\n  fastify: FastifyInstance,\n  options: {\n    requireAuth: boolean\n    showClientLogs: boolean\n    clientLogGroupName?: string | null\n  },\n) => {\n  const { requireAuth, showClientLogs, clientLogGroupName } = options\n\n  const cloudWatchLogger = new CloudWatchLogger(clientLogGroupName)\n  await cloudWatchLogger.ensureStream()\n\n  fastify.post('/logs', async (request, reply) => {\n    const body = request.body as { events?: LogEvent[] } | undefined\n\n    if (!body || !Array.isArray(body.events)) {\n      reply.code(400).send({ error: 'Invalid body: { events: LogEvent[] }' })\n      return\n    }\n\n    const events = body.events\n    const userSub = (requireAuth && (request as any).user?.sub) || undefined\n\n    const now = Date.now()\n    const entries = events.map(e => {\n      const ts = typeof e.ts === 'number' ? e.ts : now\n      const level =\n        e.level === 'trace' ||\n        e.level === 'debug' ||\n        e.level === 'info' ||\n        e.level === 'warn' ||\n        e.level === 'error' ||\n        e.level === 'fatal' ||\n        e.level === 'log'\n          ? e.level\n          : 'info'\n      const structured = {\n        source: e.source || 'client',\n        level,\n        ts,\n        message: e.message,\n        fields: e.fields || {},\n        interactionId: e.interactionId,\n        traceId: e.traceId,\n        spanId: e.spanId,\n        appVersion: e.appVersion,\n        platform: e.platform,\n        userSub,\n      }\n      return {\n        timestamp: ts,\n        message: JSON.stringify(structured),\n      }\n    })\n\n    const sent = await cloudWatchLogger.sendLogs(entries)\n\n    if (!sent) {\n      if (!showClientLogs) {\n        reply.code(204).send()\n        return\n      }\n      for (const e of entries) {\n        try {\n          process.stdout.write(`${e.message}\\n`)\n        } catch (err) {\n          fastify.log.error({ err }, 'Failed to write client log to stdout')\n        }\n      }\n    }\n\n    reply.code(204).send()\n  })\n}\n"
  },
  {
    "path": "server/src/services/loggingInterceptor.ts",
    "content": "import type { Interceptor } from '@connectrpc/connect'\nimport { ConnectError, Code } from '@connectrpc/connect'\n\nexport const loggingInterceptor: Interceptor = next => async req => {\n  try {\n    return await next(req)\n  } catch (err) {\n    // Don't throw cancellations as errors - they're expected\n    if (err instanceof ConnectError && err.code === Code.Canceled) {\n      console.log(`🚫 [${new Date().toISOString()}] RPC cancelled: ${req.url}`)\n    } else {\n      console.error(\n        `❌ [${new Date().toISOString()}] RPC failed: ${req.url}`,\n        err,\n      )\n    }\n    throw err\n  }\n}\n"
  },
  {
    "path": "server/src/services/stripeWebhook.test.ts",
    "content": "import { describe, it, expect, mock, beforeEach, afterEach } from 'bun:test'\nimport {\n  type AnyObject,\n  createTestApp,\n  createEnvReset,\n} from './__tests__/helpers.js'\n\nconst mockStripeState: {\n  webhooksConstructEvent: AnyObject | null\n  subscriptionsRetrieve: AnyObject | null\n  shouldThrow: string | null\n  constructEventShouldThrow: boolean\n  eventToReturn: AnyObject | null\n} = {\n  webhooksConstructEvent: null,\n  subscriptionsRetrieve: null,\n  shouldThrow: null,\n  constructEventShouldThrow: false,\n  eventToReturn: null,\n}\n\nmock.module('stripe', () => {\n  class Stripe {\n    webhooks: any\n    subscriptions: any\n\n    constructor(_apiKey: string) {\n      this.webhooks = {\n        constructEvent: (\n          rawBody: Buffer | string,\n          signature: string,\n          secret: string,\n        ) => {\n          if (mockStripeState.constructEventShouldThrow) {\n            mockStripeState.constructEventShouldThrow = false\n            throw new Error('Invalid signature')\n          }\n          mockStripeState.webhooksConstructEvent = {\n            rawBody,\n            signature,\n            secret,\n          }\n          // If eventToReturn is set, use it; otherwise try to parse from rawBody\n          if (mockStripeState.eventToReturn) {\n            return mockStripeState.eventToReturn\n          }\n          // Try to parse from rawBody\n          if (typeof rawBody === 'string') {\n            try {\n              return JSON.parse(rawBody)\n            } catch {\n              // Fallback\n            }\n          }\n          // Default fallback\n          return {\n            type: 'checkout.session.completed',\n            data: {\n              object: {\n                id: 'cs_test_123',\n                mode: 'subscription',\n                customer: 'cus_test_123',\n                subscription: 'sub_test_123',\n                metadata: { user_sub: 'user-123' },\n              },\n            },\n          }\n        },\n        constructEventAsync: async (\n          rawBody: Buffer | string,\n          signature: string,\n          secret: string,\n        ) => {\n          if (mockStripeState.constructEventShouldThrow) {\n            mockStripeState.constructEventShouldThrow = false\n            throw new Error('Invalid signature')\n          }\n          mockStripeState.webhooksConstructEvent = {\n            rawBody,\n            signature,\n            secret,\n          }\n          // If eventToReturn is set, use it; otherwise try to parse from rawBody\n          if (mockStripeState.eventToReturn) {\n            return mockStripeState.eventToReturn\n          }\n          // Try to parse from rawBody\n          if (typeof rawBody === 'string') {\n            try {\n              return JSON.parse(rawBody)\n            } catch {\n              // Fallback\n            }\n          }\n          // Default fallback\n          return {\n            type: 'checkout.session.completed',\n            data: {\n              object: {\n                id: 'cs_test_123',\n                mode: 'subscription',\n                customer: 'cus_test_123',\n                subscription: 'sub_test_123',\n                metadata: { user_sub: 'user-123' },\n              },\n            },\n          }\n        },\n      }\n\n      this.subscriptions = {\n        retrieve: async (subscriptionId: string) => {\n          if (mockStripeState.shouldThrow === 'subscriptions.retrieve') {\n            mockStripeState.shouldThrow = null\n            throw new Error('Stripe API error')\n          }\n          return {\n            id: subscriptionId,\n            items: {\n              data: [\n                {\n                  current_period_start: Math.floor(Date.now() / 1000),\n                },\n              ],\n            },\n            ...mockStripeState.subscriptionsRetrieve,\n          }\n        },\n      }\n    }\n  }\n  return {\n    default: Stripe,\n    __mockStripeState: mockStripeState,\n  }\n})\n\nconst mockSubscriptionsRepo: {\n  upsertActive: AnyObject | null\n  deleteByStripeSubscriptionId: boolean\n} = {\n  upsertActive: null,\n  deleteByStripeSubscriptionId: false,\n}\n\nconst mockTrialsRepo: {\n  getByStripeSubscriptionId: AnyObject | null\n  upsertFromStripeSubscription: AnyObject | null\n  completeTrial: boolean\n  shouldThrow: string | null\n} = {\n  getByStripeSubscriptionId: null,\n  upsertFromStripeSubscription: null,\n  completeTrial: false,\n  shouldThrow: null,\n}\n\nmock.module('../db/repo.js', () => {\n  return {\n    SubscriptionsRepository: {\n      upsertActive: async (\n        userId: string,\n        stripeCustomerId: string | null,\n        stripeSubscriptionId: string | null,\n        startAt: Date | null,\n      ) => {\n        if (mockSubscriptionsRepo.upsertActive === null) {\n          return {\n            user_id: userId,\n            stripe_customer_id: stripeCustomerId,\n            stripe_subscription_id: stripeSubscriptionId,\n            subscription_start_at: startAt,\n          }\n        }\n        if (typeof mockSubscriptionsRepo.upsertActive === 'function') {\n          return mockSubscriptionsRepo.upsertActive(\n            userId,\n            stripeCustomerId,\n            stripeSubscriptionId,\n            startAt,\n          )\n        }\n        return mockSubscriptionsRepo.upsertActive\n      },\n      deleteByStripeSubscriptionId: async (subscriptionId: string) => {\n        mockSubscriptionsRepo.deleteByStripeSubscriptionId = true\n        return true\n      },\n    },\n    TrialsRepository: {\n      getByStripeSubscriptionId: async (subscriptionId: string) => {\n        if (mockTrialsRepo.shouldThrow === 'getByStripeSubscriptionId') {\n          mockTrialsRepo.shouldThrow = null\n          throw new Error('Database error')\n        }\n        if (mockTrialsRepo.getByStripeSubscriptionId === null) {\n          return null\n        }\n        if (typeof mockTrialsRepo.getByStripeSubscriptionId === 'function') {\n          return mockTrialsRepo.getByStripeSubscriptionId(subscriptionId)\n        }\n        return mockTrialsRepo.getByStripeSubscriptionId\n      },\n      upsertFromStripeSubscription: async (\n        userId: string,\n        subscriptionId: string,\n        trialStartAt: Date | null,\n        hasCompletedTrial: boolean,\n      ) => {\n        if (mockTrialsRepo.shouldThrow === 'upsertFromStripeSubscription') {\n          mockTrialsRepo.shouldThrow = null\n          throw new Error('Database error')\n        }\n        if (mockTrialsRepo.upsertFromStripeSubscription === null) {\n          return {\n            user_id: userId,\n            stripe_subscription_id: subscriptionId,\n            trial_start_at: trialStartAt,\n            has_completed_trial: hasCompletedTrial,\n          }\n        }\n        if (typeof mockTrialsRepo.upsertFromStripeSubscription === 'function') {\n          return mockTrialsRepo.upsertFromStripeSubscription(\n            userId,\n            subscriptionId,\n            trialStartAt,\n            hasCompletedTrial,\n          )\n        }\n        return mockTrialsRepo.upsertFromStripeSubscription\n      },\n      completeTrial: async (userId: string) => {\n        if (mockTrialsRepo.shouldThrow === 'completeTrial') {\n          mockTrialsRepo.shouldThrow = null\n          throw new Error('Database error')\n        }\n        mockTrialsRepo.completeTrial = true\n        return {\n          user_id: userId,\n          has_completed_trial: true,\n        }\n      },\n    },\n  }\n})\n\nconst envReset = createEnvReset()\n\nimport { registerStripeWebhook } from './stripeWebhook.js'\n\ndescribe('registerStripeWebhook', () => {\n  beforeEach(() => {\n    envReset.set({\n      STRIPE_SECRET_KEY: 'sk_test_123',\n      STRIPE_WEBHOOK_SECRET: 'whsec_test_123',\n    })\n    mockStripeState.webhooksConstructEvent = null\n    mockStripeState.subscriptionsRetrieve = null\n    mockStripeState.shouldThrow = null\n    mockStripeState.constructEventShouldThrow = false\n    mockStripeState.eventToReturn = null\n    mockSubscriptionsRepo.upsertActive = null\n    mockSubscriptionsRepo.deleteByStripeSubscriptionId = false\n    mockTrialsRepo.getByStripeSubscriptionId = null\n    mockTrialsRepo.upsertFromStripeSubscription = null\n    mockTrialsRepo.completeTrial = false\n    mockTrialsRepo.shouldThrow = null\n  })\n\n  afterEach(() => {\n    envReset.reset()\n  })\n\n  describe('POST /stripe/webhook', () => {\n    it('returns 400 when signature is missing', async () => {\n      const app = createTestApp()\n      await registerStripeWebhook(app)\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/stripe/webhook',\n        headers: {\n          'content-type': 'application/json',\n        },\n        payload: JSON.stringify({}),\n      })\n\n      expect(res.statusCode).toBe(400)\n      expect(res.body).toBe('Missing signature')\n      await app.close()\n    })\n\n    it('returns 400 when signature verification fails', async () => {\n      const app = createTestApp()\n      await registerStripeWebhook(app)\n\n      mockStripeState.constructEventShouldThrow = true\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/stripe/webhook',\n        headers: {\n          'content-type': 'application/json',\n          'stripe-signature': 'invalid_signature',\n        },\n        payload: JSON.stringify({}),\n      })\n\n      expect(res.statusCode).toBe(400)\n      expect(res.body).toContain('Webhook signature verification failed')\n      await app.close()\n    })\n\n    it('handles checkout.session.completed event for subscription', async () => {\n      const app = createTestApp()\n      await registerStripeWebhook(app)\n\n      const eventPayload = {\n        type: 'checkout.session.completed',\n        data: {\n          object: {\n            id: 'cs_test_123',\n            mode: 'subscription',\n            customer: 'cus_test_123',\n            subscription: 'sub_test_123',\n            metadata: { user_sub: 'user-123' },\n            client_reference_id: null,\n          },\n        },\n      }\n\n      mockStripeState.eventToReturn = eventPayload\n      mockStripeState.subscriptionsRetrieve = {\n        id: 'sub_test_123',\n        items: {\n          data: [\n            {\n              current_period_start: Math.floor(Date.now() / 1000),\n            },\n          ],\n        },\n      }\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/stripe/webhook',\n        headers: {\n          'content-type': 'application/json',\n          'stripe-signature': 'valid_signature',\n        },\n        payload: JSON.stringify(eventPayload),\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.received).toBe(true)\n      expect(mockTrialsRepo.completeTrial).toBe(true)\n      await app.close()\n    })\n\n    it('handles checkout.session.async_payment_succeeded event', async () => {\n      const app = createTestApp()\n      await registerStripeWebhook(app)\n\n      const eventPayload = {\n        type: 'checkout.session.async_payment_succeeded',\n        data: {\n          object: {\n            id: 'cs_test_123',\n            mode: 'subscription',\n            customer: 'cus_test_123',\n            subscription: 'sub_test_123',\n            metadata: null,\n            client_reference_id: 'user-123',\n          },\n        },\n      }\n\n      mockStripeState.eventToReturn = eventPayload\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/stripe/webhook',\n        headers: {\n          'content-type': 'application/json',\n          'stripe-signature': 'valid_signature',\n        },\n        payload: JSON.stringify(eventPayload),\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.received).toBe(true)\n      expect(mockTrialsRepo.completeTrial).toBe(true)\n      await app.close()\n    })\n\n    it('skips checkout.session.completed when mode is not subscription', async () => {\n      const app = createTestApp()\n      await registerStripeWebhook(app)\n\n      const eventPayload = {\n        type: 'checkout.session.completed',\n        data: {\n          object: {\n            id: 'cs_test_123',\n            mode: 'payment',\n            customer: 'cus_test_123',\n            subscription: null,\n            metadata: { user_sub: 'user-123' },\n          },\n        },\n      }\n\n      mockStripeState.eventToReturn = eventPayload\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/stripe/webhook',\n        headers: {\n          'content-type': 'application/json',\n          'stripe-signature': 'valid_signature',\n        },\n        payload: JSON.stringify(eventPayload),\n      })\n\n      expect(res.statusCode).toBe(200)\n      expect(mockTrialsRepo.completeTrial).toBe(false)\n      await app.close()\n    })\n\n    it('skips checkout.session.completed when user_sub is missing', async () => {\n      const app = createTestApp()\n      await registerStripeWebhook(app)\n\n      const eventPayload = {\n        type: 'checkout.session.completed',\n        data: {\n          object: {\n            id: 'cs_test_123',\n            mode: 'subscription',\n            customer: 'cus_test_123',\n            subscription: 'sub_test_123',\n            metadata: null,\n            client_reference_id: null,\n          },\n        },\n      }\n\n      mockStripeState.eventToReturn = eventPayload\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/stripe/webhook',\n        headers: {\n          'content-type': 'application/json',\n          'stripe-signature': 'valid_signature',\n        },\n        payload: JSON.stringify(eventPayload),\n      })\n\n      expect(res.statusCode).toBe(200)\n      expect(mockTrialsRepo.completeTrial).toBe(false)\n      await app.close()\n    })\n\n    it('handles customer.subscription.created event', async () => {\n      const app = createTestApp()\n      await registerStripeWebhook(app)\n\n      const eventPayload = {\n        type: 'customer.subscription.created',\n        data: {\n          object: {\n            id: 'sub_test_123',\n            status: 'active',\n            customer: 'cus_test_123',\n            metadata: { user_sub: 'user-123' },\n            items: {\n              data: [\n                {\n                  current_period_start: Math.floor(Date.now() / 1000),\n                },\n              ],\n            },\n          },\n        },\n      }\n\n      mockStripeState.eventToReturn = eventPayload\n      // Mock that this is not a trial subscription\n      mockTrialsRepo.getByStripeSubscriptionId = null\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/stripe/webhook',\n        headers: {\n          'content-type': 'application/json',\n          'stripe-signature': 'valid_signature',\n        },\n        payload: JSON.stringify(eventPayload),\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.received).toBe(true)\n      expect(mockTrialsRepo.completeTrial).toBe(true)\n      await app.close()\n    })\n\n    it('handles customer.subscription.updated event with active status', async () => {\n      const app = createTestApp()\n      await registerStripeWebhook(app)\n\n      const eventPayload = {\n        type: 'customer.subscription.updated',\n        data: {\n          object: {\n            id: 'sub_test_123',\n            status: 'active',\n            customer: 'cus_test_123',\n            metadata: { user_sub: 'user-123' },\n            items: {\n              data: [\n                {\n                  current_period_start: Math.floor(Date.now() / 1000),\n                },\n              ],\n            },\n          },\n        },\n      }\n\n      mockStripeState.eventToReturn = eventPayload\n      // Mock that this is not a trial subscription\n      mockTrialsRepo.getByStripeSubscriptionId = null\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/stripe/webhook',\n        headers: {\n          'content-type': 'application/json',\n          'stripe-signature': 'valid_signature',\n        },\n        payload: JSON.stringify(eventPayload),\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.received).toBe(true)\n      expect(mockTrialsRepo.completeTrial).toBe(true)\n      await app.close()\n    })\n\n    it('handles customer.subscription.updated event with canceled status', async () => {\n      const app = createTestApp()\n      await registerStripeWebhook(app)\n\n      const eventPayload = {\n        type: 'customer.subscription.updated',\n        data: {\n          object: {\n            id: 'sub_test_123',\n            status: 'canceled',\n            customer: 'cus_test_123',\n            metadata: { user_sub: 'user-123' },\n          },\n        },\n      }\n\n      mockStripeState.eventToReturn = eventPayload\n      // Mock that this is not a trial subscription\n      mockTrialsRepo.getByStripeSubscriptionId = null\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/stripe/webhook',\n        headers: {\n          'content-type': 'application/json',\n          'stripe-signature': 'valid_signature',\n        },\n        payload: JSON.stringify(eventPayload),\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.received).toBe(true)\n      expect(mockSubscriptionsRepo.deleteByStripeSubscriptionId).toBe(true)\n      expect(mockTrialsRepo.completeTrial).toBe(false)\n      await app.close()\n    })\n\n    it('skips customer.subscription.updated when status is not active', async () => {\n      const app = createTestApp()\n      await registerStripeWebhook(app)\n\n      const eventPayload = {\n        type: 'customer.subscription.updated',\n        data: {\n          object: {\n            id: 'sub_test_123',\n            status: 'past_due',\n            customer: 'cus_test_123',\n            metadata: { user_sub: 'user-123' },\n          },\n        },\n      }\n\n      mockStripeState.eventToReturn = eventPayload\n      // Mock that this is not a trial subscription\n      mockTrialsRepo.getByStripeSubscriptionId = null\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/stripe/webhook',\n        headers: {\n          'content-type': 'application/json',\n          'stripe-signature': 'valid_signature',\n        },\n        payload: JSON.stringify(eventPayload),\n      })\n\n      expect(res.statusCode).toBe(200)\n      expect(mockTrialsRepo.completeTrial).toBe(false)\n      expect(mockSubscriptionsRepo.deleteByStripeSubscriptionId).toBe(false)\n      await app.close()\n    })\n\n    it('skips customer.subscription events when user_sub is missing', async () => {\n      const app = createTestApp()\n      await registerStripeWebhook(app)\n\n      const eventPayload = {\n        type: 'customer.subscription.created',\n        data: {\n          object: {\n            id: 'sub_test_123',\n            status: 'active',\n            customer: 'cus_test_123',\n            metadata: {},\n            items: {\n              data: [\n                {\n                  current_period_start: Math.floor(Date.now() / 1000),\n                },\n              ],\n            },\n          },\n        },\n      }\n\n      mockStripeState.eventToReturn = eventPayload\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/stripe/webhook',\n        headers: {\n          'content-type': 'application/json',\n          'stripe-signature': 'valid_signature',\n        },\n        payload: JSON.stringify(eventPayload),\n      })\n\n      expect(res.statusCode).toBe(200)\n      expect(mockTrialsRepo.completeTrial).toBe(false)\n      await app.close()\n    })\n\n    it('handles customer.subscription.deleted event', async () => {\n      const app = createTestApp()\n      await registerStripeWebhook(app)\n\n      const eventPayload = {\n        type: 'customer.subscription.deleted',\n        data: {\n          object: {\n            id: 'sub_test_123',\n            status: 'canceled',\n            customer: 'cus_test_123',\n          },\n        },\n      }\n\n      mockStripeState.eventToReturn = eventPayload\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/stripe/webhook',\n        headers: {\n          'content-type': 'application/json',\n          'stripe-signature': 'valid_signature',\n        },\n        payload: JSON.stringify(eventPayload),\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.received).toBe(true)\n      expect(mockSubscriptionsRepo.deleteByStripeSubscriptionId).toBe(true)\n      await app.close()\n    })\n\n    it('handles customer.subscription.deleted when id is missing', async () => {\n      const app = createTestApp()\n      await registerStripeWebhook(app)\n\n      const eventPayload = {\n        type: 'customer.subscription.deleted',\n        data: {\n          object: {\n            id: null,\n            status: 'canceled',\n            customer: 'cus_test_123',\n          },\n        },\n      }\n\n      mockStripeState.eventToReturn = eventPayload\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/stripe/webhook',\n        headers: {\n          'content-type': 'application/json',\n          'stripe-signature': 'valid_signature',\n        },\n        payload: JSON.stringify(eventPayload),\n      })\n\n      expect(res.statusCode).toBe(200)\n      expect(mockSubscriptionsRepo.deleteByStripeSubscriptionId).toBe(false)\n      await app.close()\n    })\n\n    it('handles unknown event types gracefully', async () => {\n      const app = createTestApp()\n      await registerStripeWebhook(app)\n\n      const eventPayload = {\n        type: 'payment_intent.succeeded',\n        data: {\n          object: {\n            id: 'pi_test_123',\n          },\n        },\n      }\n\n      mockStripeState.eventToReturn = eventPayload\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/stripe/webhook',\n        headers: {\n          'content-type': 'application/json',\n          'stripe-signature': 'valid_signature',\n        },\n        payload: JSON.stringify(eventPayload),\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.received).toBe(true)\n      await app.close()\n    })\n\n    it('handles processing errors gracefully', async () => {\n      const app = createTestApp()\n      await registerStripeWebhook(app)\n\n      const eventPayload = {\n        type: 'checkout.session.completed',\n        data: {\n          object: {\n            id: 'cs_test_123',\n            mode: 'subscription',\n            customer: 'cus_test_123',\n            subscription: 'sub_test_123',\n            metadata: { user_sub: 'user-123' },\n          },\n        },\n      }\n\n      mockStripeState.eventToReturn = eventPayload\n      mockStripeState.shouldThrow = 'subscriptions.retrieve'\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/stripe/webhook',\n        headers: {\n          'content-type': 'application/json',\n          'stripe-signature': 'valid_signature',\n        },\n        payload: JSON.stringify(eventPayload),\n      })\n\n      expect(res.statusCode).toBe(500)\n      const body = JSON.parse(res.body)\n      expect(body.error).toBe('Internal error')\n      await app.close()\n    })\n\n    it('handles customer.subscription.created event for trial subscription', async () => {\n      const app = createTestApp()\n      await registerStripeWebhook(app)\n\n      const eventPayload = {\n        type: 'customer.subscription.created',\n        data: {\n          object: {\n            id: 'sub_test_123',\n            status: 'trialing',\n            customer: 'cus_test_123',\n            metadata: { user_sub: 'user-123' },\n            trial_start: Math.floor(Date.now() / 1000),\n            items: {\n              data: [\n                {\n                  current_period_start: Math.floor(Date.now() / 1000),\n                },\n              ],\n            },\n          },\n        },\n      }\n\n      mockStripeState.eventToReturn = eventPayload\n      // Mock that this IS a trial subscription\n      mockTrialsRepo.getByStripeSubscriptionId = {\n        user_id: 'user-123',\n        stripe_subscription_id: 'sub_test_123',\n      }\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/stripe/webhook',\n        headers: {\n          'content-type': 'application/json',\n          'stripe-signature': 'valid_signature',\n        },\n        payload: JSON.stringify(eventPayload),\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.received).toBe(true)\n      // Should update trial but not complete it (status is trialing)\n      expect(mockTrialsRepo.completeTrial).toBe(false)\n      await app.close()\n    })\n\n    it('handles customer.subscription.trial_will_end event', async () => {\n      const app = createTestApp()\n      await registerStripeWebhook(app)\n\n      const eventPayload = {\n        type: 'customer.subscription.trial_will_end',\n        data: {\n          object: {\n            id: 'sub_test_123',\n            status: 'trialing',\n            customer: 'cus_test_123',\n            metadata: { user_sub: 'user-123' },\n            trial_start: Math.floor(Date.now() / 1000),\n          },\n        },\n      }\n\n      mockStripeState.eventToReturn = eventPayload\n      // Mock that this IS a trial subscription\n      mockTrialsRepo.getByStripeSubscriptionId = {\n        user_id: 'user-123',\n        stripe_subscription_id: 'sub_test_123',\n      }\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/stripe/webhook',\n        headers: {\n          'content-type': 'application/json',\n          'stripe-signature': 'valid_signature',\n        },\n        payload: JSON.stringify(eventPayload),\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.received).toBe(true)\n      // Should update trial status but not complete it\n      expect(mockTrialsRepo.completeTrial).toBe(false)\n      await app.close()\n    })\n\n    it('handles customer.subscription.paused event', async () => {\n      const app = createTestApp()\n      await registerStripeWebhook(app)\n\n      const eventPayload = {\n        type: 'customer.subscription.paused',\n        data: {\n          object: {\n            id: 'sub_test_123',\n            status: 'paused',\n            customer: 'cus_test_123',\n            metadata: { user_sub: 'user-123' },\n          },\n        },\n      }\n\n      mockStripeState.eventToReturn = eventPayload\n      // Mock that this IS a trial subscription\n      mockTrialsRepo.getByStripeSubscriptionId = {\n        user_id: 'user-123',\n        stripe_subscription_id: 'sub_test_123',\n      }\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/stripe/webhook',\n        headers: {\n          'content-type': 'application/json',\n          'stripe-signature': 'valid_signature',\n        },\n        payload: JSON.stringify(eventPayload),\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.received).toBe(true)\n      // Should mark trial as completed (paused due to no payment method)\n      expect(mockTrialsRepo.completeTrial).toBe(false)\n      await app.close()\n    })\n\n    it('handles customer.subscription.deleted event for trial subscription', async () => {\n      const app = createTestApp()\n      await registerStripeWebhook(app)\n\n      const eventPayload = {\n        type: 'customer.subscription.deleted',\n        data: {\n          object: {\n            id: 'sub_test_123',\n            status: 'canceled',\n            customer: 'cus_test_123',\n            metadata: { user_sub: 'user-123' },\n          },\n        },\n      }\n\n      mockStripeState.eventToReturn = eventPayload\n      // Mock that this IS a trial subscription\n      mockTrialsRepo.getByStripeSubscriptionId = {\n        user_id: 'user-123',\n        stripe_subscription_id: 'sub_test_123',\n      }\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/stripe/webhook',\n        headers: {\n          'content-type': 'application/json',\n          'stripe-signature': 'valid_signature',\n        },\n        payload: JSON.stringify(eventPayload),\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.received).toBe(true)\n      expect(mockSubscriptionsRepo.deleteByStripeSubscriptionId).toBe(true)\n      // Should update trial status to completed\n      expect(mockTrialsRepo.completeTrial).toBe(false)\n      await app.close()\n    })\n  })\n})\n"
  },
  {
    "path": "server/src/services/stripeWebhook.ts",
    "content": "import type { FastifyInstance } from 'fastify'\nimport Stripe from 'stripe'\nimport { SubscriptionsRepository, TrialsRepository } from '../db/repo.js'\n\nexport const registerStripeWebhook = async (fastify: FastifyInstance) => {\n  const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY\n  const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET\n  if (!STRIPE_SECRET_KEY) throw new Error('Missing STRIPE_SECRET_KEY')\n  if (!STRIPE_WEBHOOK_SECRET) throw new Error('Missing STRIPE_WEBHOOK_SECRET')\n\n  const stripe = new Stripe(STRIPE_SECRET_KEY)\n\n  // Scope a child instance to avoid global parser conflicts\n  await fastify.register(async function (f) {\n    f.addContentTypeParser(\n      'application/json',\n      { parseAs: 'buffer' },\n      (req, body, done) => {\n        try {\n          ;(req as any).rawBody = body\n          const json = body.length ? JSON.parse(body.toString()) : {}\n          done(null, json)\n        } catch (err) {\n          done(err as any, undefined)\n        }\n      },\n    )\n\n    // Public webhook endpoint\n    f.post('/stripe/webhook', async (request, reply) => {\n      const sig = request.headers['stripe-signature'] as string | undefined\n      if (!sig) {\n        reply.code(400).send('Missing signature')\n        return\n      }\n\n      let event: Stripe.Event\n      try {\n        const raw = (request as any).rawBody || (request as any).body\n        event = await stripe.webhooks.constructEventAsync(\n          raw,\n          sig,\n          STRIPE_WEBHOOK_SECRET,\n        )\n      } catch (err: any) {\n        reply\n          .code(400)\n          .send(`Webhook signature verification failed: ${err?.message}`)\n        return\n      }\n\n      try {\n        switch (event.type) {\n          case 'checkout.session.completed':\n          case 'checkout.session.async_payment_succeeded': {\n            const session = event.data.object as Stripe.Checkout.Session\n            if (session.mode !== 'subscription') break\n            const userSub = (session.metadata?.user_sub ||\n              session.client_reference_id) as string | undefined\n            const stripeCustomerId = (session.customer as string) || null\n            const stripeSubscriptionId =\n              (session.subscription as string) || null\n            if (!userSub || !stripeSubscriptionId) break\n\n            const sub =\n              await stripe.subscriptions.retrieve(stripeSubscriptionId)\n            const startSec = sub.items.data[0]?.current_period_start || null\n            const subscriptionStartAt = startSec\n              ? new Date(startSec * 1000)\n              : null\n\n            // Sync cancel_at_period_end status\n            // When cancel_at_period_end is true, cancel_at contains the period end date\n            const subscriptionEndAt =\n              sub.cancel_at_period_end && sub.cancel_at\n                ? new Date(sub.cancel_at * 1000)\n                : null\n\n            await SubscriptionsRepository.upsertActive(\n              userSub,\n              stripeCustomerId,\n              stripeSubscriptionId,\n              subscriptionStartAt,\n              subscriptionEndAt,\n            )\n            await TrialsRepository.completeTrial(userSub)\n            break\n          }\n\n          case 'customer.subscription.created':\n          case 'customer.subscription.updated': {\n            const sub = event.data.object as Stripe.Subscription\n            const userSub = (sub.metadata?.user_sub || sub.metadata?.user) as\n              | string\n              | undefined\n            if (!userSub) break\n\n            // Check if this is a trial subscription\n            const trialRow = await TrialsRepository.getByStripeSubscriptionId(\n              sub.id,\n            )\n            if (trialRow) {\n              // Sync trial status from Stripe\n              const trialStartAt = sub.trial_start\n                ? new Date(sub.trial_start * 1000)\n                : null\n              const trialEndAt = sub.trial_end\n                ? new Date(sub.trial_end * 1000)\n                : null\n              const hasCompletedTrial =\n                sub.status === 'active' ||\n                sub.status === 'past_due' ||\n                sub.status === 'canceled' ||\n                sub.status === 'incomplete_expired'\n\n              await TrialsRepository.upsertFromStripeSubscription(\n                userSub,\n                sub.id,\n                trialStartAt,\n                hasCompletedTrial,\n                trialEndAt,\n              )\n            }\n\n            if (sub.status === 'canceled') {\n              if (sub.id) {\n                await SubscriptionsRepository.deleteByStripeSubscriptionId(\n                  sub.id,\n                )\n              }\n              break\n            }\n            if (sub.status !== 'active') break\n\n            const stripeCustomerId = sub.customer as string\n            const stripeSubscriptionId = sub.id\n            const startSec = sub.items.data[0]?.current_period_start || null\n            const subscriptionStartAt = startSec\n              ? new Date(startSec * 1000)\n              : null\n\n            // Sync cancel_at_period_end status\n            // When cancel_at_period_end is true, cancel_at contains the period end date\n            const subscriptionEndAt =\n              sub.cancel_at_period_end && sub.cancel_at\n                ? new Date(sub.cancel_at * 1000)\n                : null\n\n            await SubscriptionsRepository.upsertActive(\n              userSub,\n              stripeCustomerId,\n              stripeSubscriptionId,\n              subscriptionStartAt,\n              subscriptionEndAt,\n            )\n            // Mark trial as completed when subscription becomes active\n            if (sub.status === 'active') {\n              await TrialsRepository.completeTrial(userSub)\n            }\n            break\n          }\n\n          case 'customer.subscription.deleted': {\n            const sub = event.data.object as Stripe.Subscription\n            const userSub = (sub.metadata?.user_sub || sub.metadata?.user) as\n              | string\n              | undefined\n\n            // Update trial status if this was a trial subscription\n            if (userSub) {\n              const trialRow = await TrialsRepository.getByStripeSubscriptionId(\n                sub.id,\n              )\n              if (trialRow) {\n                const trialEndAt = sub.trial_end\n                  ? new Date(sub.trial_end * 1000)\n                  : null\n                await TrialsRepository.upsertFromStripeSubscription(\n                  userSub,\n                  sub.id,\n                  null,\n                  true, // Trial completed (canceled)\n                  trialEndAt,\n                )\n              }\n            }\n\n            if (sub.id) {\n              await SubscriptionsRepository.deleteByStripeSubscriptionId(sub.id)\n            }\n            break\n          }\n\n          case 'customer.subscription.trial_will_end': {\n            const sub = event.data.object as Stripe.Subscription\n            const userSub = (sub.metadata?.user_sub || sub.metadata?.user) as\n              | string\n              | undefined\n            if (!userSub) break\n\n            // Sync trial status - trial is ending soon\n            const trialRow = await TrialsRepository.getByStripeSubscriptionId(\n              sub.id,\n            )\n            if (trialRow) {\n              const trialStartAt = sub.trial_start\n                ? new Date(sub.trial_start * 1000)\n                : null\n              const trialEndAt = sub.trial_end\n                ? new Date(sub.trial_end * 1000)\n                : null\n              await TrialsRepository.upsertFromStripeSubscription(\n                userSub,\n                sub.id,\n                trialStartAt,\n                false, // Still in trial\n                trialEndAt,\n              )\n            }\n            break\n          }\n\n          case 'customer.subscription.paused': {\n            const sub = event.data.object as Stripe.Subscription\n            const userSub = (sub.metadata?.user_sub || sub.metadata?.user) as\n              | string\n              | undefined\n            if (!userSub) break\n\n            // Trial ended without payment method and was paused\n            const trialRow = await TrialsRepository.getByStripeSubscriptionId(\n              sub.id,\n            )\n            if (trialRow) {\n              const trialEndAt = sub.trial_end\n                ? new Date(sub.trial_end * 1000)\n                : null\n              await TrialsRepository.upsertFromStripeSubscription(\n                userSub,\n                sub.id,\n                null,\n                true, // Trial completed (paused due to no payment method)\n                trialEndAt,\n              )\n            }\n            break\n          }\n        }\n\n        reply.code(200).send({ received: true })\n      } catch (err: any) {\n        fastify.log.error({ err }, 'Stripe webhook processing failed')\n        reply.code(500).send({ error: 'Internal error' })\n      }\n    })\n  })\n}\n\nexport default registerStripeWebhook\n"
  },
  {
    "path": "server/src/services/timing/ServerTimingCollector.ts",
    "content": "import { performance } from 'perf_hooks'\nimport { S3StorageClient } from '../../clients/s3storageClient.js'\n\n/**\n * Enum for server-side timing events in the transcription pipeline\n */\nexport enum ServerTimingEventName {\n  STREAM_COLLECTION = 'server_stream_collection',\n  AUDIO_PROCESSING = 'server_audio_processing',\n  ASR_TRANSCRIPTION = 'server_asr_transcription',\n  LLM_ADJUSTMENT = 'server_llm_adjustment',\n  TOTAL_PROCESSING = 'server_total_processing',\n}\n\n// Configuration for S3\nconst TIMING_BUCKET = process.env.TIMING_BUCKET\n\n// Initialize storage client for timing bucket\nlet timingStorageClient: S3StorageClient | null = null\nif (TIMING_BUCKET) {\n  try {\n    timingStorageClient = new S3StorageClient(TIMING_BUCKET)\n  } catch (error) {\n    console.error(\n      '[ServerTimingCollector] Failed to initialize timing storage client:',\n      error,\n    )\n  }\n}\n\ninterface TimingEvent {\n  name: string\n  startMs: number\n  endMs?: number\n  durationMs?: number\n}\n\ninterface TimingReport {\n  interactionId: string\n  userId: string\n  events: TimingEvent[]\n  source: 'server'\n}\n\ninterface ActiveTiming {\n  interactionId: string\n  userId: string\n  startTimestamp: string\n  events: Map<ServerTimingEventName, TimingEvent>\n}\n\n/**\n * ServerTimingCollector for collecting server-side transcription pipeline timing data\n */\nexport class ServerTimingCollector {\n  private activeTimings = new Map<string, ActiveTiming>()\n  private completedReports: TimingReport[] = []\n  private flushTimer: NodeJS.Timeout | null = null\n  private FIRST_EVENT = ServerTimingEventName.TOTAL_PROCESSING\n\n  // Configuration - more aggressive flushing for server to reduce memory\n  private readonly FLUSH_INTERVAL_MS = 2_000 // 2 seconds\n  private readonly BATCH_SIZE = 5 // Smaller batches\n  private readonly MAX_QUEUE_SIZE = 50\n\n  constructor() {\n    this.scheduleFlush()\n    console.log('[ServerTimingCollector] Service initialized')\n  }\n\n  /**\n   * Start a new timing session for a transcription request\n   */\n  startInteraction(interactionId?: string, userId?: string) {\n    if (!interactionId) {\n      console.warn(\n        '[ServerTimingCollector] Cannot start timing: no interaction ID provided',\n      )\n      return\n    }\n\n    this.activeTimings.set(interactionId, {\n      interactionId,\n      userId: userId || 'unknown',\n      startTimestamp: new Date().toISOString(),\n      events: new Map(),\n    })\n  }\n\n  /**\n   * Start timing for a specific event\n   */\n  startTiming(eventName: ServerTimingEventName, interactionId?: string) {\n    if (!interactionId) {\n      return\n    }\n\n    const active = this.activeTimings.get(interactionId)\n    if (!active) {\n      console.warn(\n        `[ServerTimingCollector] Cannot start timing for unknown interaction: ${interactionId}`,\n      )\n      return\n    }\n\n    const timingEvent: TimingEvent = {\n      name: eventName,\n      startMs: performance.now(),\n    }\n    active.events.set(eventName, timingEvent)\n  }\n\n  /**\n   * End timing for a specific event\n   */\n  endTiming(eventName: ServerTimingEventName, interactionId?: string) {\n    if (!interactionId) {\n      return\n    }\n\n    const active = this.activeTimings.get(interactionId)\n    if (!active) {\n      console.warn(\n        `[ServerTimingCollector] Cannot end timing for unknown interaction: ${interactionId}`,\n      )\n      return\n    }\n\n    const timingEvent = active.events.get(eventName)\n    if (!timingEvent) {\n      console.warn(\n        `[ServerTimingCollector] Cannot end timing for unknown event: ${eventName}`,\n      )\n      return\n    }\n\n    timingEvent.endMs = performance.now()\n    timingEvent.durationMs = timingEvent.endMs - timingEvent.startMs\n  }\n\n  /**\n   * Finalize an interaction and move it to completed reports\n   */\n  finalizeInteraction(interactionId?: string) {\n    if (!interactionId) {\n      console.warn(\n        '[ServerTimingCollector] Cannot finalize: no interaction ID provided',\n      )\n      return\n    }\n\n    const active = this.activeTimings.get(interactionId)\n    if (!active) {\n      console.warn(\n        `[ServerTimingCollector] Cannot finalize unknown interaction: ${interactionId}`,\n      )\n      return\n    }\n\n    // Calculate total duration\n    const events = Array.from(active.events.values())\n    const firstEvent = events.find(e => e.name === this.FIRST_EVENT)\n    const lastEvent = events.reduce((latest, event) => {\n      const eventEnd = event.endMs || event.startMs\n      const latestEnd = latest.endMs || latest.startMs\n      return eventEnd > latestEnd ? event : latest\n    }, events[0])\n\n    const totalDuration = firstEvent\n      ? (lastEvent.endMs || lastEvent.startMs) - firstEvent.startMs\n      : 0\n\n    // Create timing report\n    const report: TimingReport = {\n      interactionId,\n      userId: active.userId,\n      events,\n      source: 'server',\n    }\n\n    // Remove from active and add to completed\n    this.activeTimings.delete(interactionId)\n    this.completedReports.push(report)\n\n    // Enforce max queue size\n    if (this.completedReports.length > this.MAX_QUEUE_SIZE) {\n      console.warn(\n        `[ServerTimingCollector] Queue size exceeded ${this.MAX_QUEUE_SIZE}, dropping oldest reports`,\n      )\n      this.completedReports = this.completedReports.slice(-this.MAX_QUEUE_SIZE)\n    }\n\n    console.log(\n      `[ServerTimingCollector] Finalized interaction: ${interactionId} (${events.length} events, ${totalDuration}ms total)`,\n    )\n\n    // Check if we should flush\n    if (this.completedReports.length >= this.BATCH_SIZE) {\n      this.flush()\n    }\n  }\n\n  /**\n   * Clear an interaction without finalizing (for errors/cancellations)\n   */\n  clearInteraction(interactionId?: string) {\n    if (!interactionId) {\n      return\n    }\n\n    this.activeTimings.delete(interactionId)\n    console.log(`[ServerTimingCollector] Cleared interaction: ${interactionId}`)\n  }\n\n  /**\n   * Flush completed reports to S3\n   */\n  async flush({ flushAll = false } = {}) {\n    if (this.completedReports.length === 0) {\n      return\n    }\n\n    if (!timingStorageClient) {\n      console.warn(\n        '[ServerTimingCollector] No timing storage client configured, skipping flush',\n      )\n      this.completedReports = [] // Clear reports to avoid memory leak\n      return\n    }\n\n    const reportsToSend = this.completedReports.splice(\n      0,\n      flushAll ? this.completedReports.length : this.BATCH_SIZE,\n    )\n\n    console.log(\n      `[ServerTimingCollector] Flushing ${reportsToSend.length} timing reports to S3`,\n    )\n\n    // Upload each report to S3\n    const uploadPromises = reportsToSend.map(async report => {\n      const timingData = {\n        source: 'server',\n        interactionId: report.interactionId,\n        userId: report.userId,\n        timestamp: new Date().toISOString(),\n        events: report.events.map(event => ({\n          name: event.name,\n          startMs: event.startMs,\n          endMs: event.endMs,\n          durationMs: event.durationMs,\n        })),\n      }\n\n      // S3 key pattern: server/{interaction-id}/{timestamp}.json\n      const key = `server/${report.interactionId}/${Date.now()}.json`\n\n      try {\n        await timingStorageClient.uploadObject(\n          key,\n          JSON.stringify(timingData),\n          'application/json',\n        )\n        console.log(\n          `[ServerTimingCollector] Uploaded server timing to S3: ${key}`,\n        )\n      } catch (error) {\n        console.error(\n          `[ServerTimingCollector] Failed to upload timing to S3: ${key}`,\n          error,\n        )\n        throw error // Will be caught by Promise.allSettled below\n      }\n    })\n\n    // Wait for all uploads, but don't fail if some fail\n    const results = await Promise.allSettled(uploadPromises)\n\n    const successCount = results.filter(r => r.status === 'fulfilled').length\n    const failCount = results.filter(r => r.status === 'rejected').length\n\n    if (failCount > 0) {\n      console.error(\n        `[ServerTimingCollector] Failed to upload ${failCount}/${reportsToSend.length} reports`,\n      )\n      // Re-add failed reports to the front of the queue for retry\n      const failedReports = reportsToSend.filter(\n        (_, i) => results[i].status === 'rejected',\n      )\n      this.completedReports.unshift(...failedReports)\n    }\n\n    console.log(\n      `[ServerTimingCollector] Successfully submitted ${successCount}/${reportsToSend.length} reports`,\n    )\n  }\n\n  /**\n   * Schedule periodic flushing\n   */\n  private scheduleFlush() {\n    if (!this.flushTimer) {\n      this.flushTimer = setInterval(() => {\n        this.flush()\n      }, this.FLUSH_INTERVAL_MS)\n    }\n  }\n\n  /**\n   * Stop periodic flushing and flush any remaining reports\n   */\n  async shutdown() {\n    if (this.flushTimer) {\n      clearInterval(this.flushTimer)\n      this.flushTimer = null\n    }\n\n    // Flush any remaining reports\n    await this.flush({ flushAll: true })\n\n    console.log('[ServerTimingCollector] Service shutdown complete')\n  }\n\n  /**\n   * Utility function to wrap an async operation with automatic timing\n   * Handles both successful and error cases automatically\n   */\n  async timeAsync<T>(\n    eventName: ServerTimingEventName,\n    fn: () => Promise<T> | T,\n    interactionId?: string,\n  ): Promise<T> {\n    this.startTiming(eventName, interactionId)\n    try {\n      const result = await fn()\n      return result\n    } finally {\n      // Always end timing, even if the function throws\n      this.endTiming(eventName, interactionId)\n    }\n  }\n}\n\nexport const serverTimingCollector = new ServerTimingCollector()\n"
  },
  {
    "path": "server/src/services/trial.test.ts",
    "content": "import { describe, it, expect, mock, beforeEach, afterEach } from 'bun:test'\nimport {\n  type AnyObject,\n  createTestAppWithAuth,\n  createTestApp,\n  createEnvReset,\n} from './__tests__/helpers.js'\n\nconst mockTrialsRepo: {\n  getByUserId: AnyObject | null\n  upsertFromStripeSubscription: AnyObject | null\n  completeTrial: AnyObject | null\n  shouldThrow: string | null\n} = {\n  getByUserId: null,\n  upsertFromStripeSubscription: null,\n  completeTrial: null,\n  shouldThrow: null,\n}\n\nconst mockSubscriptionsRepo: {\n  getByUserId: AnyObject | null\n} = {\n  getByUserId: null,\n}\n\nconst mockStripe: {\n  customers: {\n    search: any\n    create: any\n    update: any\n  }\n  subscriptions: {\n    create: any\n    retrieve: any\n    list: any\n  }\n} = {\n  customers: {\n    search: null,\n    create: null,\n    update: null,\n  },\n  subscriptions: {\n    create: null,\n    retrieve: null,\n    list: null,\n  },\n}\n\nconst mockFetch = mock(() => Promise.resolve(new Response()))\n\nmock.module('../db/repo.js', () => {\n  return {\n    TrialsRepository: {\n      getByUserId: async (userId: string) => {\n        if (mockTrialsRepo.shouldThrow === 'getByUserId') {\n          mockTrialsRepo.shouldThrow = null\n          throw new Error('Database error')\n        }\n        if (mockTrialsRepo.getByUserId === null) {\n          return null\n        }\n        if (typeof mockTrialsRepo.getByUserId === 'function') {\n          return mockTrialsRepo.getByUserId(userId)\n        }\n        return mockTrialsRepo.getByUserId\n      },\n      upsertFromStripeSubscription: async (\n        userId: string,\n        subscriptionId: string,\n        trialStartAt: Date | null,\n        hasCompletedTrial: boolean,\n        trialEndAt?: Date | null,\n      ) => {\n        if (mockTrialsRepo.shouldThrow === 'upsertFromStripeSubscription') {\n          mockTrialsRepo.shouldThrow = null\n          throw new Error('Database error')\n        }\n        if (mockTrialsRepo.upsertFromStripeSubscription === null) {\n          return {\n            user_id: userId,\n            stripe_subscription_id: subscriptionId,\n            trial_start_at: trialStartAt,\n            trial_end_at: trialEndAt ?? null,\n            has_completed_trial: hasCompletedTrial,\n          }\n        }\n        if (typeof mockTrialsRepo.upsertFromStripeSubscription === 'function') {\n          return mockTrialsRepo.upsertFromStripeSubscription(\n            userId,\n            subscriptionId,\n            trialStartAt,\n            hasCompletedTrial,\n            trialEndAt,\n          )\n        }\n        return mockTrialsRepo.upsertFromStripeSubscription\n      },\n      completeTrial: async (userId: string) => {\n        if (mockTrialsRepo.shouldThrow === 'completeTrial') {\n          mockTrialsRepo.shouldThrow = null\n          throw new Error('Database error')\n        }\n        if (mockTrialsRepo.completeTrial === null) {\n          return {\n            user_id: userId,\n            trial_start_at: null,\n            has_completed_trial: true,\n          }\n        }\n        if (typeof mockTrialsRepo.completeTrial === 'function') {\n          return mockTrialsRepo.completeTrial(userId)\n        }\n        return mockTrialsRepo.completeTrial\n      },\n    },\n    SubscriptionsRepository: {\n      getByUserId: async (userId: string) => {\n        if (mockSubscriptionsRepo.getByUserId === null) {\n          return null\n        }\n        if (typeof mockSubscriptionsRepo.getByUserId === 'function') {\n          return mockSubscriptionsRepo.getByUserId(userId)\n        }\n        return mockSubscriptionsRepo.getByUserId\n      },\n    },\n  }\n})\n\nmock.module('stripe', () => {\n  return {\n    default: class MockStripe {\n      customers = {\n        search: mockStripe.customers.search || (async () => ({ data: [] })),\n        create:\n          mockStripe.customers.create ||\n          (async () => ({ id: 'cus_test123', email: null, name: null })),\n        update:\n          mockStripe.customers.update ||\n          (async () => ({ id: 'cus_test123', email: null, name: null })),\n      }\n      subscriptions = {\n        create:\n          mockStripe.subscriptions.create ||\n          (async () => {\n            const now = Math.floor(Date.now() / 1000)\n            const trialEnd = now + 14 * 24 * 60 * 60\n            return {\n              id: 'sub_test123',\n              status: 'trialing',\n              trial_start: now,\n              trial_end: trialEnd,\n              items: { data: [{ price: { id: 'price_test123' } }] },\n            }\n          }),\n        retrieve:\n          mockStripe.subscriptions.retrieve ||\n          (async () => {\n            const now = Math.floor(Date.now() / 1000)\n            const trialEnd = now + 14 * 24 * 60 * 60\n            return {\n              id: 'sub_test123',\n              status: 'trialing',\n              trial_start: now,\n              trial_end: trialEnd,\n              items: { data: [{ price: { id: 'price_test123' } }] },\n            }\n          }),\n        list: mockStripe.subscriptions.list || (async () => ({ data: [] })),\n      }\n    },\n  }\n})\n\n// Mock fetch for Auth0 API calls\nglobal.fetch = mockFetch as any\n\nimport { registerTrialRoutes } from './trial.js'\n\ndescribe('registerTrialRoutes', () => {\n  const envReset = createEnvReset()\n\n  beforeEach(() => {\n    mockTrialsRepo.getByUserId = null\n    mockTrialsRepo.upsertFromStripeSubscription = null\n    mockTrialsRepo.completeTrial = null\n    mockTrialsRepo.shouldThrow = null\n    mockSubscriptionsRepo.getByUserId = null\n    mockStripe.customers.search = null\n    mockStripe.customers.create = null\n    mockStripe.customers.update = null\n    mockStripe.subscriptions.create = null\n    mockStripe.subscriptions.retrieve = null\n    mockStripe.subscriptions.list = null\n    mockFetch.mockClear()\n\n    // Set up default environment variables\n    envReset.set({\n      STRIPE_SECRET_KEY: 'sk_test_123',\n      STRIPE_PRICE_ID: 'price_test123',\n      AUTH0_DOMAIN: 'test.auth0.com',\n      AUTH0_MGMT_CLIENT_ID: 'test_client_id',\n      AUTH0_MGMT_CLIENT_SECRET: 'test_client_secret',\n    })\n\n    // Mock Auth0 Management API calls\n    mockFetch.mockImplementation((url: string | Request | URL) => {\n      const urlStr = typeof url === 'string' ? url : url.toString()\n      if (urlStr.includes('/oauth/token')) {\n        return Promise.resolve(\n          new Response(JSON.stringify({ access_token: 'mock_token' }), {\n            status: 200,\n          }),\n        )\n      }\n      if (urlStr.includes('/api/v2/users/')) {\n        return Promise.resolve(\n          new Response(\n            JSON.stringify({\n              email: 'test@example.com',\n              name: 'Test User',\n            }),\n            { status: 200 },\n          ),\n        )\n      }\n      return Promise.resolve(new Response('{}', { status: 200 }))\n    })\n  })\n\n  afterEach(() => {\n    envReset.reset()\n  })\n\n  describe('POST /trial/start', () => {\n    it('returns 401 when requireAuth is true and user is missing', async () => {\n      const app = createTestApp()\n      await registerTrialRoutes(app, { requireAuth: true })\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/trial/start',\n      })\n\n      expect(res.statusCode).toBe(401)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Unauthorized')\n      await app.close()\n    })\n\n    it('returns 500 when Stripe is not configured', async () => {\n      envReset.set({\n        STRIPE_SECRET_KEY: undefined,\n        STRIPE_PRICE_ID: undefined,\n      })\n      const app = createTestAppWithAuth()\n      await registerTrialRoutes(app, { requireAuth: true })\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/trial/start',\n      })\n\n      expect(res.statusCode).toBe(500)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Stripe not configured')\n      await app.close()\n    })\n\n    it('returns existing trial status when user already has a subscription', async () => {\n      const app = createTestAppWithAuth()\n      await registerTrialRoutes(app, { requireAuth: true })\n\n      const trialStartAt = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000)\n      mockTrialsRepo.getByUserId = {\n        user_id: 'user-123',\n        stripe_subscription_id: 'sub_existing123',\n        trial_start_at: trialStartAt,\n        has_completed_trial: false,\n      }\n\n      const now = Math.floor(Date.now() / 1000)\n      const trialEnd = now + 9 * 24 * 60 * 60\n      mockStripe.subscriptions.retrieve = async () => ({\n        id: 'sub_existing123',\n        status: 'trialing',\n        trial_start: Math.floor(trialStartAt.getTime() / 1000),\n        trial_end: trialEnd,\n        items: { data: [{ price: { id: 'price_test123' } }] },\n      })\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/trial/start',\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(true)\n      expect(body.trialDays).toBe(14)\n      expect(body.daysLeft).toBeGreaterThan(0)\n      expect(body.daysLeft).toBeLessThanOrEqual(14)\n      expect(body.isTrialActive).toBe(true)\n      expect(body.hasCompletedTrial).toBe(false)\n      await app.close()\n    })\n\n    it('returns completed trial status when trial is already completed', async () => {\n      const app = createTestAppWithAuth()\n      await registerTrialRoutes(app, { requireAuth: true })\n\n      mockTrialsRepo.getByUserId = {\n        user_id: 'user-123',\n        stripe_subscription_id: null,\n        trial_start_at: null,\n        has_completed_trial: true,\n      }\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/trial/start',\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(true)\n      expect(body.trialDays).toBe(14)\n      expect(body.trialStartAt).toBe(null)\n      expect(body.daysLeft).toBe(0)\n      expect(body.isTrialActive).toBe(false)\n      expect(body.hasCompletedTrial).toBe(true)\n      await app.close()\n    })\n\n    it('creates new trial subscription successfully', async () => {\n      const app = createTestAppWithAuth()\n      await registerTrialRoutes(app, { requireAuth: true })\n\n      mockTrialsRepo.getByUserId = null // No existing trial\n      mockSubscriptionsRepo.getByUserId = null // No existing subscription\n      mockStripe.customers.search = async () => ({ data: [] }) // No existing customer\n      mockStripe.customers.create = async () => ({\n        id: 'cus_new123',\n        email: 'test@example.com',\n        name: 'Test User',\n      })\n      mockStripe.subscriptions.list = async () => ({ data: [] }) // No existing subscriptions\n\n      const now = Math.floor(Date.now() / 1000)\n      const trialEnd = now + 14 * 24 * 60 * 60\n      mockStripe.subscriptions.create = async () => ({\n        id: 'sub_new123',\n        status: 'trialing',\n        trial_start: now,\n        trial_end: trialEnd,\n        items: { data: [{ price: { id: 'price_test123' } }] },\n      })\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/trial/start',\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(true)\n      expect(body.trialDays).toBe(14)\n      expect(body.daysLeft).toBe(14)\n      expect(body.isTrialActive).toBe(true)\n      expect(body.hasCompletedTrial).toBe(false)\n      await app.close()\n    })\n\n    it('uses existing Stripe customer when found', async () => {\n      const app = createTestAppWithAuth()\n      await registerTrialRoutes(app, { requireAuth: true })\n\n      mockTrialsRepo.getByUserId = null\n      mockSubscriptionsRepo.getByUserId = null\n      mockStripe.customers.search = async () => ({\n        data: [{ id: 'cus_existing123' }],\n      })\n      mockStripe.subscriptions.list = async () => ({ data: [] }) // No existing subscriptions\n\n      const now = Math.floor(Date.now() / 1000)\n      const trialEnd = now + 14 * 24 * 60 * 60\n      mockStripe.subscriptions.create = async () => ({\n        id: 'sub_new123',\n        status: 'trialing',\n        trial_start: now,\n        trial_end: trialEnd,\n        items: { data: [{ price: { id: 'price_test123' } }] },\n      })\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/trial/start',\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(true)\n      await app.close()\n    })\n\n    it('handles database errors gracefully', async () => {\n      const app = createTestAppWithAuth()\n      await registerTrialRoutes(app, { requireAuth: true })\n\n      mockTrialsRepo.shouldThrow = 'getByUserId'\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/trial/start',\n      })\n\n      expect(res.statusCode).toBe(500)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Database error')\n      await app.close()\n    })\n\n    it('handles upsert errors gracefully', async () => {\n      const app = createTestAppWithAuth()\n      await registerTrialRoutes(app, { requireAuth: true })\n\n      mockTrialsRepo.getByUserId = null\n      mockSubscriptionsRepo.getByUserId = null\n      mockStripe.customers.search = async () => ({ data: [] })\n      mockStripe.customers.create = async () => ({\n        id: 'cus_new123',\n      })\n      mockStripe.subscriptions.list = async () => ({ data: [] }) // No existing subscriptions\n\n      const now = Math.floor(Date.now() / 1000)\n      const trialEnd = now + 14 * 24 * 60 * 60\n      mockStripe.subscriptions.create = async () => ({\n        id: 'sub_new123',\n        status: 'trialing',\n        trial_start: now,\n        trial_end: trialEnd,\n        items: { data: [{ price: { id: 'price_test123' } }] },\n      })\n\n      mockTrialsRepo.shouldThrow = 'upsertFromStripeSubscription'\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/trial/start',\n      })\n\n      expect(res.statusCode).toBe(500)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Database error')\n      await app.close()\n    })\n  })\n\n  describe('POST /trial/complete', () => {\n    it('returns 401 when requireAuth is true and user is missing', async () => {\n      const app = createTestApp()\n      await registerTrialRoutes(app, { requireAuth: true })\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/trial/complete',\n      })\n\n      expect(res.statusCode).toBe(401)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Unauthorized')\n      await app.close()\n    })\n\n    it('completes trial successfully when authenticated', async () => {\n      const app = createTestAppWithAuth()\n      await registerTrialRoutes(app, { requireAuth: true })\n\n      mockTrialsRepo.completeTrial = {\n        user_id: 'user-123',\n        trial_start_at: null,\n        has_completed_trial: true,\n      }\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/trial/complete',\n      })\n\n      expect(res.statusCode).toBe(200)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(true)\n      expect(body.trialDays).toBe(14)\n      expect(body.trialStartAt).toBe(null)\n      expect(body.daysLeft).toBe(0)\n      expect(body.isTrialActive).toBe(false)\n      expect(body.hasCompletedTrial).toBe(true)\n      await app.close()\n    })\n\n    it('handles errors gracefully', async () => {\n      const app = createTestAppWithAuth()\n      await registerTrialRoutes(app, { requireAuth: true })\n\n      mockTrialsRepo.shouldThrow = 'completeTrial'\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/trial/complete',\n      })\n\n      expect(res.statusCode).toBe(500)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Database error')\n      await app.close()\n    })\n\n    it('handles errors with no message', async () => {\n      const app = createTestAppWithAuth()\n      await registerTrialRoutes(app, { requireAuth: true })\n\n      mockTrialsRepo.completeTrial = () => {\n        throw new Error()\n      }\n\n      const res = await app.inject({\n        method: 'POST',\n        url: '/trial/complete',\n      })\n\n      expect(res.statusCode).toBe(500)\n      const body = JSON.parse(res.body)\n      expect(body.success).toBe(false)\n      expect(body.error).toBe('Server error')\n      await app.close()\n    })\n  })\n})\n"
  },
  {
    "path": "server/src/services/trial.ts",
    "content": "import type { FastifyInstance } from 'fastify'\nimport Stripe from 'stripe'\nimport { TrialsRepository, SubscriptionsRepository } from '../db/repo.js'\nimport {\n  getAuth0ManagementToken,\n  getUserInfoFromAuth0,\n} from '../auth/auth0Helpers.js'\n\nconst TRIAL_DAYS = 14\nconst MS_PER_DAY = 24 * 60 * 60 * 1000\n\nasync function getOrCreateStripeCustomer(\n  stripe: Stripe,\n  userSub: string,\n  email?: string,\n  name?: string,\n): Promise<string> {\n  // Check if user already has a subscription with a customer ID\n  const existingSub = await SubscriptionsRepository.getByUserId(userSub)\n  if (existingSub?.stripe_customer_id) {\n    // Update existing customer with name/email if provided\n    if (name || email) {\n      await stripe.customers.update(existingSub.stripe_customer_id, {\n        email: email,\n        name: name,\n        metadata: { user_sub: userSub },\n      })\n    }\n    return existingSub.stripe_customer_id\n  }\n\n  // Search for existing customer by metadata\n  const existingCustomers = await stripe.customers.search({\n    query: `metadata['user_sub']:'${userSub}'`,\n    limit: 1,\n  })\n\n  if (existingCustomers.data.length > 0) {\n    const customerId = existingCustomers.data[0].id\n    // Update existing customer with name/email if provided\n    console.log('Updating existing customer with name/email', name, email)\n    if (name || email) {\n      await stripe.customers.update(customerId, {\n        email: email ?? undefined,\n        name: name ?? undefined,\n        metadata: { user_sub: userSub },\n      })\n    }\n    return customerId\n  }\n\n  // Create new customer\n  const customer = await stripe.customers.create({\n    email: email,\n    name: name,\n    metadata: { user_sub: userSub },\n  })\n\n  return customer.id\n}\n\nfunction computeStatusFromStripe(subscription: Stripe.Subscription): {\n  success: boolean\n  trialDays: number\n  trialStartAt: string | null\n  daysLeft: number\n  isTrialActive: boolean\n  hasCompletedTrial: boolean\n} {\n  const now = Date.now()\n  const trialEnd = subscription.trial_end\n    ? new Date(subscription.trial_end * 1000)\n    : null\n  const trialStart = trialEnd\n    ? new Date(trialEnd.getTime() - TRIAL_DAYS * MS_PER_DAY)\n    : null\n\n  let daysLeft = 0\n  let isTrialActive = false\n\n  if (trialEnd && subscription.status === 'trialing') {\n    const elapsedMs = now - trialStart!.getTime()\n    const elapsedDays = Math.floor(elapsedMs / MS_PER_DAY)\n    daysLeft = Math.max(0, TRIAL_DAYS - elapsedDays)\n    isTrialActive = daysLeft > 0\n  }\n\n  const hasCompletedTrial =\n    subscription.status === 'active' ||\n    subscription.status === 'past_due' ||\n    subscription.status === 'canceled'\n\n  return {\n    success: true,\n    trialDays: TRIAL_DAYS,\n    trialStartAt: trialStart ? trialStart.toISOString() : null,\n    daysLeft,\n    isTrialActive,\n    hasCompletedTrial,\n  }\n}\n\nfunction computeStatus(row: {\n  trial_start_at: Date | null\n  has_completed_trial: boolean\n}) {\n  const now = Date.now()\n  const trialStartAt = row.trial_start_at ? new Date(row.trial_start_at) : null\n  let daysLeft = 0\n  if (trialStartAt && !row.has_completed_trial) {\n    const elapsedDays = Math.floor((now - trialStartAt.getTime()) / MS_PER_DAY)\n    daysLeft = Math.max(0, TRIAL_DAYS - elapsedDays)\n  }\n  const isTrialActive =\n    !!trialStartAt && !row.has_completed_trial && daysLeft > 0\n\n  return {\n    success: true,\n    trialDays: TRIAL_DAYS,\n    trialStartAt: trialStartAt ? trialStartAt.toISOString() : null,\n    daysLeft,\n    isTrialActive,\n    hasCompletedTrial: row.has_completed_trial,\n  }\n}\n\nexport const registerTrialRoutes = async (\n  fastify: FastifyInstance,\n  options: { requireAuth: boolean },\n) => {\n  const { requireAuth } = options\n\n  const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY\n  const STRIPE_PRICE_ID = process.env.STRIPE_PRICE_ID\n\n  if (!STRIPE_SECRET_KEY || !STRIPE_PRICE_ID) {\n    fastify.log.warn(\n      'Stripe credentials not configured; trial routes will fail',\n    )\n  }\n\n  const stripe = STRIPE_SECRET_KEY ? new Stripe(STRIPE_SECRET_KEY) : null\n\n  fastify.post('/trial/start', async (request, reply) => {\n    console.log('trial/start', request.body)\n    try {\n      const userSub = (requireAuth && (request as any).user?.sub) || undefined\n      if (!userSub) {\n        reply.code(401).send({ success: false, error: 'Unauthorized' })\n        return\n      }\n\n      if (!stripe) {\n        reply.code(500).send({ success: false, error: 'Stripe not configured' })\n        return\n      }\n\n      // Check if user already has a trial subscription\n      const existingTrial = await TrialsRepository.getByUserId(userSub)\n      if (existingTrial?.stripe_subscription_id) {\n        // Fetch current status from Stripe\n        const subscription = await stripe.subscriptions.retrieve(\n          existingTrial.stripe_subscription_id,\n        )\n        const status = computeStatusFromStripe(subscription)\n\n        // Sync status to database\n        const trialEndAt = subscription.trial_end\n          ? new Date(subscription.trial_end * 1000)\n          : null\n        await TrialsRepository.upsertFromStripeSubscription(\n          userSub,\n          subscription.id,\n          status.trialStartAt ? new Date(status.trialStartAt) : null,\n          status.hasCompletedTrial,\n          trialEndAt,\n        )\n\n        reply.send(status)\n        return\n      }\n\n      // Check if user already completed trial\n      if (existingTrial?.has_completed_trial) {\n        reply.send(computeStatus(existingTrial))\n        return\n      }\n\n      // Get user info from Auth0 Management API (access token only has 'sub')\n      const auth0UserInfo = await getUserInfoFromAuth0(userSub)\n      const userEmail = auth0UserInfo?.email\n      const userName = auth0UserInfo?.name\n\n      const stripeCustomerId = await getOrCreateStripeCustomer(\n        stripe,\n        userSub,\n        userEmail,\n        userName,\n      )\n\n      // Check if customer already has an active or trialing subscription for this product\n      const existingSubscriptions = await stripe.subscriptions.list({\n        customer: stripeCustomerId,\n        status: 'all',\n        limit: 100,\n      })\n\n      const hasActiveTrialSubscription = existingSubscriptions.data.some(\n        sub =>\n          (sub.status === 'trialing' || sub.status === 'active') &&\n          sub.items.data.some(item => item.price.id === STRIPE_PRICE_ID),\n      )\n\n      if (hasActiveTrialSubscription) {\n        // Find the active/trialing subscription and sync it to the database\n        const activeSubscription = existingSubscriptions.data.find(\n          sub =>\n            (sub.status === 'trialing' || sub.status === 'active') &&\n            sub.items.data.some(item => item.price.id === STRIPE_PRICE_ID),\n        )\n\n        if (activeSubscription) {\n          const status = computeStatusFromStripe(activeSubscription)\n          const trialEndAt = activeSubscription.trial_end\n            ? new Date(activeSubscription.trial_end * 1000)\n            : null\n          await TrialsRepository.upsertFromStripeSubscription(\n            userSub,\n            activeSubscription.id,\n            status.trialStartAt ? new Date(status.trialStartAt) : null,\n            status.hasCompletedTrial,\n            trialEndAt,\n          )\n          reply.send(status)\n          return\n        }\n      }\n\n      // Create subscription with trial\n      const subscription = await stripe.subscriptions.create({\n        customer: stripeCustomerId,\n        items: [{ price: STRIPE_PRICE_ID }],\n        trial_period_days: TRIAL_DAYS,\n        trial_settings: {\n          end_behavior: {\n            missing_payment_method: 'cancel',\n          },\n        },\n        metadata: { user_sub: userSub },\n      })\n\n      // Store trial in database\n      const trialStartAt = subscription.trial_start\n        ? new Date(subscription.trial_start * 1000)\n        : null\n      const trialEndAt = subscription.trial_end\n        ? new Date(subscription.trial_end * 1000)\n        : null\n      const row = await TrialsRepository.upsertFromStripeSubscription(\n        userSub,\n        subscription.id,\n        trialStartAt,\n        false,\n        trialEndAt,\n      )\n\n      const status = computeStatusFromStripe(subscription)\n      reply.send(status)\n    } catch (error: any) {\n      fastify.log.error({ err: error }, 'Trial start failed')\n      reply\n        .code(500)\n        .send({ success: false, error: error?.message || 'Server error' })\n    }\n  })\n\n  fastify.post('/trial/complete', async (request, reply) => {\n    try {\n      const userSub = (requireAuth && (request as any).user?.sub) || undefined\n      if (!userSub) {\n        reply.code(401).send({ success: false, error: 'Unauthorized' })\n        return\n      }\n      const row = await TrialsRepository.completeTrial(userSub)\n      reply.send(computeStatus(row))\n    } catch (error: any) {\n      reply\n        .code(500)\n        .send({ success: false, error: error?.message || 'Server error' })\n    }\n  })\n}\n"
  },
  {
    "path": "server/src/services/validationInterceptor.test.ts",
    "content": "import { describe, it, expect, mock } from 'bun:test'\nimport { ConnectError } from '@connectrpc/connect'\nimport { createValidator } from '@bufbuild/protovalidate'\nimport { createValidationInterceptor } from './validationInterceptor.js'\nimport {\n  AudioChunkSchema,\n  CreateNoteRequestSchema,\n} from '../generated/ito_pb.js'\nimport { create } from '@bufbuild/protobuf'\n\ndescribe('ValidationInterceptor', () => {\n  const interceptor = createValidationInterceptor()\n\n  describe('unary request validation', () => {\n    it('should pass valid unary requests through', async () => {\n      const validRequest = create(CreateNoteRequestSchema, {\n        id: 'test-id',\n        interactionId: 'interaction-id',\n        content: 'Test note content',\n      })\n\n      const mockNext = mock(() => Promise.resolve({ message: 'success' }))\n      const mockReq = {\n        method: {\n          kind: 'unary' as const,\n          input: CreateNoteRequestSchema,\n        },\n        message: validRequest,\n      }\n\n      const result = await interceptor(mockNext)(mockReq as any)\n\n      expect(mockNext).toHaveBeenCalledWith(mockReq)\n      expect(result).toEqual({ message: 'success' })\n    })\n\n    it('should pass through requests without validation rules', async () => {\n      // Create a request with empty fields (no validation rules for strings)\n      const request = create(CreateNoteRequestSchema, {\n        id: '',\n        interactionId: '',\n        content: '',\n      })\n\n      const mockNext = mock(() => Promise.resolve({ message: 'success' }))\n      const mockReq = {\n        method: {\n          kind: 'unary' as const,\n          input: CreateNoteRequestSchema,\n        },\n        message: request,\n      }\n\n      const result = await interceptor(mockNext)(mockReq as any)\n      expect(result).toEqual({ message: 'success' })\n    })\n  })\n\n  describe('streaming request validation', () => {\n    it('should pass valid audio chunks through', async () => {\n      const validChunk = create(AudioChunkSchema, {\n        audioData: new Uint8Array(100 * 1024), // 100KB - under 1MB limit\n      })\n\n      async function* validStream() {\n        yield validChunk\n      }\n\n      const mockNext = mock(() => Promise.resolve({ message: 'success' }))\n      const mockReq = {\n        method: {\n          kind: 'client_streaming' as const,\n          input: AudioChunkSchema,\n        },\n        message: validStream(),\n      }\n\n      const result = await interceptor(mockNext)(mockReq as any)\n\n      expect(mockNext).toHaveBeenCalledWith(mockReq)\n      expect(result).toEqual({ message: 'success' })\n    })\n\n    it('should validate audio chunk size with protovalidate', () => {\n      const validator = createValidator()\n\n      // Test valid chunk (under 1MB limit)\n      const validChunk = create(AudioChunkSchema, {\n        audioData: new Uint8Array(100 * 1024), // 100KB\n      })\n\n      const validResult = validator.validate(AudioChunkSchema, validChunk)\n      expect(validResult.kind).toBe('valid')\n      expect(validResult.violations).toBeUndefined()\n\n      // Test invalid chunk (over 1MB limit)\n      const invalidChunk = create(AudioChunkSchema, {\n        audioData: new Uint8Array(2 * 1024 * 1024), // 2MB - exceeds 1MB limit\n      })\n\n      const invalidResult = validator.validate(AudioChunkSchema, invalidChunk)\n      expect(invalidResult.kind).toBe('invalid')\n      expect(invalidResult.violations).toBeDefined()\n      expect(invalidResult.violations.length).toBe(1)\n      expect(invalidResult.violations[0].message).toContain(\n        'value must be at most 1048576 bytes',\n      )\n    })\n\n    it('should reject oversized audio chunks in interceptor', async () => {\n      const oversizedChunk = create(AudioChunkSchema, {\n        audioData: new Uint8Array(2 * 1024 * 1024), // 2MB - exceeds 1MB limit\n      })\n\n      async function* invalidStream() {\n        yield oversizedChunk\n      }\n\n      const mockNext = mock(() => Promise.resolve({ message: 'success' }))\n      const mockReq = {\n        method: {\n          kind: 'client_streaming' as const,\n          input: AudioChunkSchema,\n        },\n        message: invalidStream(),\n      }\n\n      // The interceptor modifies the stream but doesn't consume it\n      // We need to manually consume the modified stream to trigger validation\n      const modifiedReq = { ...mockReq }\n\n      // Call the interceptor which will modify the message stream\n      await interceptor(mockNext)(modifiedReq as any)\n\n      // Now consume the modified stream to trigger validation\n      let errorThrown = false\n      try {\n        for await (const _chunk of modifiedReq.message) {\n          // This should throw during validation\n        }\n      } catch (error) {\n        errorThrown = true\n        expect(error).toBeInstanceOf(ConnectError)\n        expect(error.message).toContain('Streaming validation failed')\n      }\n      expect(errorThrown).toBe(true)\n    })\n\n    it('should accept chunks exactly at the limit', async () => {\n      const limitChunk = create(AudioChunkSchema, {\n        audioData: new Uint8Array(1024 * 1024), // Exactly 1MB\n      })\n\n      async function* limitStream() {\n        yield limitChunk\n      }\n\n      const mockNext = mock(() => Promise.resolve({ message: 'success' }))\n      const mockReq = {\n        method: {\n          kind: 'client_streaming' as const,\n          input: AudioChunkSchema,\n        },\n        message: limitStream(),\n      }\n\n      const result = await interceptor(mockNext)(mockReq as any)\n\n      expect(mockNext).toHaveBeenCalledWith(mockReq)\n      expect(result).toEqual({ message: 'success' })\n    })\n  })\n\n  describe('validation error handling', () => {\n    it('should handle validation library errors gracefully', async () => {\n      const mockNext = mock(() => Promise.resolve({ message: 'success' }))\n      const mockReq = {\n        method: {\n          kind: 'unary' as const,\n          input: { invalid: 'schema' }, // Invalid schema object\n        },\n        message: {},\n      }\n\n      // Should not throw even if validation fails internally\n      const result = await interceptor(mockNext)(mockReq as any)\n      expect(result).toEqual({ message: 'success' })\n    })\n  })\n\n  describe('non-validatable requests', () => {\n    it('should pass through server streaming requests without validation', async () => {\n      const mockNext = mock(() => Promise.resolve({ message: 'success' }))\n      const mockReq = {\n        method: {\n          kind: 'server_streaming' as const,\n          input: CreateNoteRequestSchema,\n        },\n        message: {},\n      }\n\n      const result = await interceptor(mockNext)(mockReq as any)\n\n      expect(mockNext).toHaveBeenCalledWith(mockReq)\n      expect(result).toEqual({ message: 'success' })\n    })\n\n    it('should pass through bidirectional streaming requests without validation', async () => {\n      const mockNext = mock(() => Promise.resolve({ message: 'success' }))\n      const mockReq = {\n        method: {\n          kind: 'bidi_streaming' as const,\n          input: CreateNoteRequestSchema,\n        },\n        message: {},\n      }\n\n      const result = await interceptor(mockNext)(mockReq as any)\n\n      expect(mockNext).toHaveBeenCalledWith(mockReq)\n      expect(result).toEqual({ message: 'success' })\n    })\n  })\n})\n"
  },
  {
    "path": "server/src/services/validationInterceptor.ts",
    "content": "import { Interceptor } from '@connectrpc/connect'\nimport { createValidator } from '@bufbuild/protovalidate'\nimport { ConnectError, Code } from '@connectrpc/connect'\n\nexport function createValidationInterceptor(): Interceptor {\n  const validator = createValidator()\n\n  return next => async req => {\n    if (req.method.kind === 'unary') {\n      // Validate unary requests\n      try {\n        const result = validator.validate(req.method.input, req.message)\n\n        if (result.kind === 'invalid') {\n          const errors =\n            result.violations\n              ?.map(v => `${v.field?.map(f => f.name).join('.')}: ${v.message}`)\n              .join(', ') || 'Validation failed'\n\n          throw new ConnectError(\n            `Validation failed: ${errors}`,\n            Code.InvalidArgument,\n          )\n        }\n      } catch (error) {\n        if (error instanceof ConnectError) {\n          throw error\n        }\n        console.error('Validation error:', error)\n      }\n    } else if (req.method.kind === 'client_streaming') {\n      // Validate streaming requests (like TranscribeStream with AudioChunk)\n      const originalStream = req.message\n\n      // Create a new async iterator that validates each chunk\n      req.message = (async function* () {\n        for await (const chunk of originalStream) {\n          try {\n            const result = validator.validate(req.method.input, chunk)\n\n            if (result.kind === 'invalid') {\n              const errors =\n                result.violations\n                  ?.map(\n                    v => `${v.field?.map(f => f.name).join('.')}: ${v.message}`,\n                  )\n                  .join(', ') || 'Validation failed'\n\n              throw new ConnectError(\n                `Streaming validation failed: ${errors}`,\n                Code.InvalidArgument,\n              )\n            }\n\n            yield chunk\n          } catch (error) {\n            if (error instanceof ConnectError) {\n              throw error\n            }\n            console.error('Streaming validation error:', error)\n            yield chunk // Continue with unvalidated chunk if validation itself fails\n          }\n        }\n      })()\n    }\n\n    return await next(req)\n  }\n}\n"
  },
  {
    "path": "server/src/utils/abortUtils.ts",
    "content": "import { ConnectError, Code } from '@connectrpc/connect'\n\n/**\n * Checks if an error is an abort/connection reset error caused by the client\n * cancelling the request or closing the connection.\n *\n * Common abort error patterns:\n * - Error message: \"aborted\"\n * - Error code: \"ECONNRESET\" (connection reset)\n * - Error code: \"ABORT_ERR\" (abort error)\n *\n * @param err - The error to check\n * @returns true if the error is an abort error, false otherwise\n */\nexport function isAbortError(err: unknown): boolean {\n  return (\n    err instanceof Error &&\n    (err.message === 'aborted' ||\n      (err as any).code === 'ECONNRESET' ||\n      (err as any).code === 'ABORT_ERR')\n  )\n}\n\n/**\n * Converts an abort error into a proper ConnectError with Code.Canceled.\n * This should be used when handling client-initiated cancellations.\n *\n * @param err - The original abort error\n * @param message - Optional custom message (defaults to \"Request cancelled by client\")\n * @returns A ConnectError with Code.Canceled\n */\nexport function createAbortError(\n  err: unknown,\n  message: string = 'Request cancelled by client',\n): ConnectError {\n  return new ConnectError(message, Code.Canceled, undefined, undefined, err)\n}\n"
  },
  {
    "path": "server/src/utils/audio.ts",
    "content": "/**\n * Light audio enhancement for 16-bit PCM mono at a given sample rate.\n * - Removes DC offset\n * - Applies a gentle high-pass filter (~80 Hz)\n * - Peak normalizes to ~-3 dBFS with a capped gain\n */\nexport function enhancePcm16(pcm: Buffer, sampleRate: number): Buffer {\n  if (!pcm || pcm.length < 2) return pcm\n\n  const sampleCount = Math.floor(pcm.length / 2)\n  if (sampleCount <= 0) return pcm\n\n  // Read int16 samples\n  const samples = new Int16Array(sampleCount)\n  for (let i = 0; i < sampleCount; i++) {\n    samples[i] = pcm.readInt16LE(i * 2)\n  }\n\n  // DC offset removal\n  let sum = 0\n  for (let i = 0; i < sampleCount; i++) sum += samples[i]\n  const mean = Math.trunc(sum / sampleCount)\n  if (mean !== 0) {\n    for (let i = 0; i < sampleCount; i++) {\n      samples[i] = (samples[i] - mean) as unknown as Int16Array[number]\n    }\n  }\n\n  // Gentle high-pass filter (~80 Hz)\n  const fc = 80\n  const a = Math.exp((-2 * Math.PI * fc) / sampleRate)\n  let prevX = 0\n  let prevY = 0\n  const filtered = new Float32Array(sampleCount)\n  for (let i = 0; i < sampleCount; i++) {\n    const x = samples[i]\n    const y = a * (prevY + x - prevX)\n    filtered[i] = y\n    prevX = x\n    prevY = y\n  }\n\n  // Peak normalize to ~-3 dBFS, cap max gain to ~+12 dB\n  let peak = 1\n  for (let i = 0; i < sampleCount; i++) {\n    const v = Math.abs(filtered[i])\n    if (v > peak) peak = v\n  }\n  const target = 0.707 * 32767 // ≈ -3 dBFS\n  const rawGain = target / peak\n  const gain = Math.min(rawGain, 4.0)\n\n  const out = Buffer.alloc(sampleCount * 2)\n  if (gain > 1.05) {\n    for (let i = 0; i < sampleCount; i++) {\n      const v = Math.round(filtered[i] * gain)\n      const clamped = Math.max(-32768, Math.min(32767, v))\n      out.writeInt16LE(clamped, i * 2)\n    }\n  } else {\n    for (let i = 0; i < sampleCount; i++) {\n      const v = Math.round(filtered[i])\n      const clamped = Math.max(-32768, Math.min(32767, v))\n      out.writeInt16LE(clamped, i * 2)\n    }\n  }\n\n  return out\n}\n"
  },
  {
    "path": "server/src/utils/audioProcessing.ts",
    "content": "import { enhancePcm16 } from './audio.js'\nimport { createWavHeader } from '../services/ito/audioUtils.js'\n\n/**\n * Concatenates multiple audio chunks into a single Uint8Array.\n *\n * @param audioChunks - Array of audio chunks to concatenate\n * @returns A single Uint8Array containing all concatenated audio data\n */\nexport function concatenateAudioChunks(audioChunks: Uint8Array[]): Uint8Array {\n  const totalLength = audioChunks.reduce((sum, chunk) => sum + chunk.length, 0)\n  const fullAudio = new Uint8Array(totalLength)\n  let offset = 0\n  for (const chunk of audioChunks) {\n    fullAudio.set(chunk, offset)\n    offset += chunk.length\n  }\n\n  console.log(\n    `🔧 [${new Date().toISOString()}] Concatenated audio: ${totalLength} bytes`,\n  )\n\n  return fullAudio\n}\n\n/**\n * Prepares raw PCM audio data for transcription by enhancing it and adding a WAV header.\n *\n * Audio specifications:\n * - Sample rate: 16000 Hz\n * - Bit depth: 16 bits\n * - Channels: 1 (mono)\n *\n * @param audioData - Raw PCM audio data\n * @returns Buffer containing WAV-formatted audio ready for transcription\n */\nexport function prepareAudioForTranscription(audioData: Uint8Array): Buffer {\n  const sampleRate = 16000\n  const bitDepth = 16\n  const channels = 1\n\n  const enhancedPcm = enhancePcm16(Buffer.from(audioData), sampleRate)\n  const wavHeader = createWavHeader(\n    enhancedPcm.length,\n    sampleRate,\n    channels,\n    bitDepth,\n  )\n\n  return Buffer.concat([wavHeader, enhancedPcm])\n}\n"
  },
  {
    "path": "server/src/utils/renderCallback.ts",
    "content": "const ITO_ENV = (process.env.ITO_ENV || 'prod').toLowerCase()\nconst DEEPLINK_SCHEME = ITO_ENV === 'prod' ? 'ito' : `ito-dev`\n\ninterface CallbackPageParams {\n  code: string\n  state: string\n}\n\nexport function renderCallbackPage(params: CallbackPageParams): string {\n  const authText = 'Log in successful'\n\n  return `<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Authentication Successful - Ito</title>\n    <style>\n      * {\n        margin: 0;\n        padding: 0;\n        box-sizing: border-box;\n      }\n\n      body {\n        font-family:\n          -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,\n          Cantarell, sans-serif;\n        background: #ffffff;\n        min-height: 100vh;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        color: #333;\n        text-align: center;\n      }\n\n      .logo {\n        width: 56px;\n        height: 56px;\n        background: #000000;\n        border-radius: 12px;\n        margin: 0 auto 1rem;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        color: white;\n        font-size: 24px;\n      }\n\n      .logo svg {\n        width: 36px;\n        height: 36px;\n        color: white;\n      }\n\n      .auth-text {\n        font-size: 24px;\n        color: #000000;\n        margin-bottom: 2rem;\n        font-weight: 600;\n      }\n\n      .button {\n        background: #000000;\n        color: white;\n        border: none;\n        padding: 12px 24px;\n        border-radius: 8px;\n        font-size: 16px;\n        font-weight: 500;\n        cursor: pointer;\n        margin-bottom: 1rem;\n        transition: background 0.2s;\n      }\n\n      .button:hover {\n        background: #1a202c;\n      }\n\n      .footer-text {\n        color: #718096;\n        font-size: 14px;\n      }\n\n      .footer-text a {\n        color: #3182ce;\n        text-decoration: none;\n      }\n\n      .footer-text a:hover {\n        text-decoration: underline;\n      }\n    </style>\n  </head>\n  <body>\n    <div>\n      <div class=\"logo\">\n        <svg\n          width=\"48\"\n          height=\"48\"\n          viewBox=\"0 0 141 141\"\n          fill=\"none\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n        >\n          <path\n            d=\"M125.837 88.3633C129.501 84.6822 132.622 80.4752 135.037 75.8738C138.947 68.4622 141 60.1469 141 51.7657C141 23.2206 117.787 0 89.2524 0C60.7172 0 37.5047 23.2206 37.5047 51.7657C37.5047 55.3482 37.8661 58.8322 38.5561 62.201C44.3058 62.4147 50.0227 63.8115 55.296 66.3752C53.3576 61.8888 52.2733 56.9423 52.2733 51.7493C52.2733 31.3552 68.849 14.7738 89.2359 14.7738C109.623 14.7738 126.199 31.3552 126.199 51.7493C126.199 61.9381 122.059 71.1902 115.373 77.8951C100.965 92.3073 77.5065 92.3073 63.0993 77.8951C48.6921 63.4829 25.2331 63.4829 10.8424 77.8951C4.15624 84.5836 0 93.8357 0 104.024C0 124.419 16.5757 141 36.9626 141C57.3495 141 72.6767 125.618 73.8431 106.276C68.5369 104.797 63.4278 102.513 58.6637 99.4724C58.9759 100.951 59.1402 102.463 59.1402 104.024C59.1402 116.251 49.1849 126.21 36.9626 126.21C24.7403 126.21 14.785 116.251 14.785 104.024C14.785 97.9112 17.2656 92.3566 21.274 88.3304C29.9151 79.6864 43.9937 79.6864 52.6347 88.3304C62.7214 98.4206 75.9787 103.466 89.2195 103.466C98.5669 103.466 107.865 100.919 115.882 96.1035C119.496 93.9343 122.831 91.3049 125.804 88.3304L125.837 88.3633Z\"\n            fill=\"currentColor\"\n          />\n        </svg>\n      </div>\n\n      <div class=\"auth-text\" id=\"authText\">${authText}</div>\n\n      <button class=\"button\" onclick=\"openApp()\">Open Ito</button>\n    </div>\n\n    <script>\n      function openApp() {\n        const authCode = '${params.code || ''}'\n        const state = '${params.state || ''}'\n        \n        if (authCode && state) {\n          window.location.href = \\`${DEEPLINK_SCHEME}://auth/callback?code=\\${authCode}&state=\\${state}\\`\n        } else {\n          console.error('Missing required authentication parameters')\n        }\n      }\n    </script>\n  </body>\n</html>`\n}\n"
  },
  {
    "path": "server/src/validation/HeaderValidator.test.ts",
    "content": "import { describe, it, expect } from 'bun:test'\nimport { ConnectError } from '@connectrpc/connect'\nimport { HeaderValidator } from './HeaderValidator.js'\n\ndescribe('HeaderValidator', () => {\n  describe('validateAsrModel', () => {\n    it('should return valid ASR model names', () => {\n      expect(HeaderValidator.validateAsrModel('whisper-large-v3')).toBe(\n        'whisper-large-v3',\n      )\n      expect(\n        HeaderValidator.validateAsrModel('distil-whisper-large-v3-en'),\n      ).toBe('distil-whisper-large-v3-en')\n    })\n\n    it('should trim whitespace from ASR models', () => {\n      expect(HeaderValidator.validateAsrModel('  whisper-large-v3  ')).toBe(\n        'whisper-large-v3',\n      )\n    })\n\n    it('should handle custom model names', () => {\n      expect(HeaderValidator.validateAsrModel('custom-model-v1.0')).toBe(\n        'custom-model-v1.0',\n      )\n    })\n\n    it('should throw ConnectError for invalid ASR models', () => {\n      expect(() => HeaderValidator.validateAsrModel('invalid<model>')).toThrow(\n        ConnectError,\n      )\n      expect(() => HeaderValidator.validateAsrModel('')).toThrow(ConnectError)\n      expect(() =>\n        HeaderValidator.validateAsrModel('model with spaces'),\n      ).toThrow(ConnectError)\n    })\n\n    it('should throw ConnectError for null and undefined inputs', () => {\n      expect(() => HeaderValidator.validateAsrModel(null as any)).toThrow(\n        ConnectError,\n      )\n      expect(() => HeaderValidator.validateAsrModel(undefined as any)).toThrow(\n        ConnectError,\n      )\n    })\n\n    it('should reject models that are too long', () => {\n      const longModel = 'a'.repeat(101)\n      expect(() => HeaderValidator.validateAsrModel(longModel)).toThrow()\n    })\n  })\n\n  describe('validateVocabulary', () => {\n    it('should return array of valid vocabulary words', () => {\n      const result = HeaderValidator.validateVocabulary('hello,world,test')\n      expect(result).toEqual(['hello', 'world', 'test'])\n    })\n\n    it('should handle empty input', () => {\n      expect(HeaderValidator.validateVocabulary('')).toEqual([])\n    })\n\n    it('should throw ConnectError for null and undefined inputs', () => {\n      expect(() => HeaderValidator.validateVocabulary(null as any)).toThrow(\n        ConnectError,\n      )\n      expect(() =>\n        HeaderValidator.validateVocabulary(undefined as any),\n      ).toThrow(ConnectError)\n    })\n\n    it('should trim individual words', () => {\n      const result = HeaderValidator.validateVocabulary('  hello  ,  world  ')\n      expect(result).toEqual(['hello', 'world'])\n    })\n\n    it('should filter out empty words', () => {\n      const result = HeaderValidator.validateVocabulary('hello,,world,  ,test')\n      expect(result).toEqual(['hello', 'world', 'test'])\n    })\n\n    it('should handle words with apostrophes', () => {\n      const result = HeaderValidator.validateVocabulary(\"it's,won't,can't\")\n      expect(result).toEqual([\"it's\", \"won't\", \"can't\"])\n    })\n\n    it('should throw ConnectError for invalid vocabulary', () => {\n      // Test with vocabulary that's too long\n      const longVocab = 'a'.repeat(5001)\n      expect(() => HeaderValidator.validateVocabulary(longVocab)).toThrow(\n        ConnectError,\n      )\n    })\n\n    it('should filter out words with invalid characters', () => {\n      const result = HeaderValidator.validateVocabulary(\n        'valid,<script>,another,invalid&word',\n      )\n      expect(result).toEqual(['valid', 'another'])\n    })\n\n    it('should limit to 500 words', () => {\n      const words = Array.from({ length: 600 }, (_, i) => `word${i}`).join(',')\n      const result = HeaderValidator.validateVocabulary(words)\n      expect(result).toHaveLength(500)\n    })\n\n    it('should filter out words that are too long', () => {\n      const longWord = 'a'.repeat(101)\n      const result = HeaderValidator.validateVocabulary(\n        `valid,${longWord},another`,\n      )\n      expect(result).toEqual(['valid', 'another'])\n    })\n  })\n\n  describe('validateAsrProvider', () => {\n    it('should return valid ASR provider names', () => {\n      expect(HeaderValidator.validateAsrProvider('groq')).toBe('groq')\n    })\n\n    it('should trim whitespace from ASR providers', () => {\n      expect(HeaderValidator.validateAsrProvider('  groq  ')).toBe('groq')\n    })\n\n    it('should throw ConnectError for invalid ASR providers', () => {\n      expect(() =>\n        HeaderValidator.validateAsrProvider('invalid-provider'),\n      ).toThrow(ConnectError)\n      expect(() => HeaderValidator.validateAsrProvider('')).toThrow(\n        ConnectError,\n      )\n      expect(() => HeaderValidator.validateAsrProvider('openai')).toThrow(\n        ConnectError,\n      )\n    })\n\n    it('should throw ConnectError for null and undefined inputs', () => {\n      expect(() => HeaderValidator.validateAsrProvider(null as any)).toThrow(\n        ConnectError,\n      )\n      expect(() =>\n        HeaderValidator.validateAsrProvider(undefined as any),\n      ).toThrow(ConnectError)\n    })\n  })\n\n  describe('validateAsrPrompt', () => {\n    it('should return valid ASR prompt', () => {\n      expect(HeaderValidator.validateAsrPrompt('transcribe this audio')).toBe(\n        'transcribe this audio',\n      )\n    })\n\n    it('should trim whitespace from ASR prompt', () => {\n      expect(HeaderValidator.validateAsrPrompt('  transcribe this  ')).toBe(\n        'transcribe this',\n      )\n    })\n\n    it('should handle empty prompt', () => {\n      expect(HeaderValidator.validateAsrPrompt('')).toBe('')\n    })\n\n    it('should throw ConnectError for prompts that are too long', () => {\n      const longPrompt = 'a'.repeat(101)\n      expect(() => HeaderValidator.validateAsrPrompt(longPrompt)).toThrow(\n        ConnectError,\n      )\n    })\n\n    it('should throw ConnectError for null and undefined inputs', () => {\n      expect(() => HeaderValidator.validateAsrPrompt(null as any)).toThrow(\n        ConnectError,\n      )\n      expect(() => HeaderValidator.validateAsrPrompt(undefined as any)).toThrow(\n        ConnectError,\n      )\n    })\n  })\n\n  describe('validateLlmProvider', () => {\n    it('should return valid LLM provider names', () => {\n      expect(HeaderValidator.validateLlmProvider('groq')).toBe('groq')\n    })\n\n    it('should trim whitespace from LLM providers', () => {\n      expect(HeaderValidator.validateLlmProvider('  groq  ')).toBe('groq')\n    })\n\n    it('should throw ConnectError for invalid LLM providers', () => {\n      expect(() =>\n        HeaderValidator.validateLlmProvider('invalid-provider'),\n      ).toThrow(ConnectError)\n      expect(() => HeaderValidator.validateLlmProvider('')).toThrow(\n        ConnectError,\n      )\n      expect(() => HeaderValidator.validateLlmProvider('openai')).toThrow(\n        ConnectError,\n      )\n    })\n\n    it('should throw ConnectError for null and undefined inputs', () => {\n      expect(() => HeaderValidator.validateLlmProvider(null as any)).toThrow(\n        ConnectError,\n      )\n      expect(() =>\n        HeaderValidator.validateLlmProvider(undefined as any),\n      ).toThrow(ConnectError)\n    })\n  })\n\n  describe('validateLlmModel', () => {\n    it('should return valid LLM model names', () => {\n      expect(HeaderValidator.validateLlmModel('gpt-4o')).toBe('gpt-4o')\n      expect(HeaderValidator.validateLlmModel('llama-3.1-8b')).toBe(\n        'llama-3.1-8b',\n      )\n    })\n\n    it('should trim whitespace from LLM models', () => {\n      expect(HeaderValidator.validateLlmModel('  gpt-4o  ')).toBe('gpt-4o')\n    })\n\n    it('should handle custom model names', () => {\n      expect(HeaderValidator.validateLlmModel('custom-model-v2.0')).toBe(\n        'custom-model-v2.0',\n      )\n    })\n\n    it('should throw ConnectError for invalid LLM models', () => {\n      expect(() => HeaderValidator.validateLlmModel('invalid<model>')).toThrow(\n        ConnectError,\n      )\n      expect(() => HeaderValidator.validateLlmModel('')).toThrow(ConnectError)\n      expect(() =>\n        HeaderValidator.validateLlmModel('model with spaces'),\n      ).toThrow(ConnectError)\n    })\n\n    it('should throw ConnectError for null and undefined inputs', () => {\n      expect(() => HeaderValidator.validateLlmModel(null as any)).toThrow(\n        ConnectError,\n      )\n      expect(() => HeaderValidator.validateLlmModel(undefined as any)).toThrow(\n        ConnectError,\n      )\n    })\n\n    it('should reject models that are too long', () => {\n      const longModel = 'a'.repeat(101)\n      expect(() => HeaderValidator.validateLlmModel(longModel)).toThrow(\n        ConnectError,\n      )\n    })\n  })\n\n  describe('validateLlmTemperature', () => {\n    it('should return valid temperature values', () => {\n      expect(HeaderValidator.validateLlmTemperature(0)).toBe(0)\n      expect(HeaderValidator.validateLlmTemperature(1)).toBe(1)\n      expect(HeaderValidator.validateLlmTemperature(2)).toBe(2)\n      expect(HeaderValidator.validateLlmTemperature(0.5)).toBe(0.5)\n    })\n\n    it('should throw ConnectError for temperature below 0', () => {\n      expect(() => HeaderValidator.validateLlmTemperature(-0.1)).toThrow(\n        ConnectError,\n      )\n      expect(() => HeaderValidator.validateLlmTemperature(-1)).toThrow(\n        ConnectError,\n      )\n    })\n\n    it('should throw ConnectError for temperature above 2', () => {\n      expect(() => HeaderValidator.validateLlmTemperature(2.1)).toThrow(\n        ConnectError,\n      )\n      expect(() => HeaderValidator.validateLlmTemperature(3)).toThrow(\n        ConnectError,\n      )\n    })\n\n    it('should throw ConnectError for null and undefined inputs', () => {\n      expect(() => HeaderValidator.validateLlmTemperature(null as any)).toThrow(\n        ConnectError,\n      )\n      expect(() =>\n        HeaderValidator.validateLlmTemperature(undefined as any),\n      ).toThrow(ConnectError)\n    })\n  })\n\n  describe('validateTranscriptionPrompt', () => {\n    it('should return valid transcription prompt', () => {\n      expect(\n        HeaderValidator.validateTranscriptionPrompt('transcribe this'),\n      ).toBe('transcribe this')\n    })\n\n    it('should trim whitespace from transcription prompt', () => {\n      expect(\n        HeaderValidator.validateTranscriptionPrompt('  transcribe this  '),\n      ).toBe('transcribe this')\n    })\n\n    it('should handle empty prompt', () => {\n      expect(HeaderValidator.validateTranscriptionPrompt('')).toBe('')\n    })\n\n    it('should throw ConnectError for prompts that are too long', () => {\n      const longPrompt = 'a'.repeat(1501)\n      expect(() =>\n        HeaderValidator.validateTranscriptionPrompt(longPrompt),\n      ).toThrow(ConnectError)\n    })\n\n    it('should throw ConnectError for null and undefined inputs', () => {\n      expect(() =>\n        HeaderValidator.validateTranscriptionPrompt(null as any),\n      ).toThrow(ConnectError)\n      expect(() =>\n        HeaderValidator.validateTranscriptionPrompt(undefined as any),\n      ).toThrow(ConnectError)\n    })\n  })\n\n  describe('validateEditingPrompt', () => {\n    it('should return valid editing prompt', () => {\n      expect(HeaderValidator.validateEditingPrompt('edit this text')).toBe(\n        'edit this text',\n      )\n    })\n\n    it('should trim whitespace from editing prompt', () => {\n      expect(HeaderValidator.validateEditingPrompt('  edit this  ')).toBe(\n        'edit this',\n      )\n    })\n\n    it('should handle empty prompt', () => {\n      expect(HeaderValidator.validateEditingPrompt('')).toBe('')\n    })\n\n    it('should throw ConnectError for prompts that are too long', () => {\n      const longPrompt = 'a'.repeat(1501)\n      expect(() => HeaderValidator.validateEditingPrompt(longPrompt)).toThrow(\n        ConnectError,\n      )\n    })\n\n    it('should throw ConnectError for null and undefined inputs', () => {\n      expect(() => HeaderValidator.validateEditingPrompt(null as any)).toThrow(\n        ConnectError,\n      )\n      expect(() =>\n        HeaderValidator.validateEditingPrompt(undefined as any),\n      ).toThrow(ConnectError)\n    })\n  })\n\n  describe('validateNoSpeechThreshold', () => {\n    it('should return valid no speech threshold values', () => {\n      expect(HeaderValidator.validateNoSpeechThreshold(0)).toBe(0)\n      expect(HeaderValidator.validateNoSpeechThreshold(1)).toBe(1)\n      expect(HeaderValidator.validateNoSpeechThreshold(0.5)).toBe(0.5)\n    })\n\n    it('should throw ConnectError for threshold below 0', () => {\n      expect(() => HeaderValidator.validateNoSpeechThreshold(-0.1)).toThrow(\n        ConnectError,\n      )\n      expect(() => HeaderValidator.validateNoSpeechThreshold(-1)).toThrow(\n        ConnectError,\n      )\n    })\n\n    it('should throw ConnectError for threshold above 1', () => {\n      expect(() => HeaderValidator.validateNoSpeechThreshold(1.1)).toThrow(\n        ConnectError,\n      )\n      expect(() => HeaderValidator.validateNoSpeechThreshold(2)).toThrow(\n        ConnectError,\n      )\n    })\n\n    it('should throw ConnectError for null and undefined inputs', () => {\n      expect(() =>\n        HeaderValidator.validateNoSpeechThreshold(null as any),\n      ).toThrow(ConnectError)\n      expect(() =>\n        HeaderValidator.validateNoSpeechThreshold(undefined as any),\n      ).toThrow(ConnectError)\n    })\n  })\n})\n"
  },
  {
    "path": "server/src/validation/HeaderValidator.ts",
    "content": "import { ConnectError, Code } from '@connectrpc/connect'\nimport {\n  AsrModelSchema,\n  AsrPromptSchema,\n  AsrProviderSchema,\n  LlmModelSchema,\n  LlmPromptSchema,\n  LlmProviderSchema,\n  LLMTemperatureSchema,\n  NoSpeechThresholdSchema,\n  VocabularySchema,\n} from './schemas.js'\n\n/**\n * Validates gRPC header values using Zod schemas\n */\nexport class HeaderValidator {\n  static validateAsrModel(headerValue: string): string {\n    try {\n      return AsrModelSchema.parse(headerValue)\n    } catch (error) {\n      throw new ConnectError(\n        `Invalid ASR model: ${error instanceof Error ? error.message : 'Unknown error'}`,\n        Code.InvalidArgument,\n      )\n    }\n  }\n\n  static validateAsrProvider(headerValue: string): string {\n    try {\n      return AsrProviderSchema.parse(headerValue)\n    } catch (error) {\n      throw new ConnectError(\n        `Invalid ASR provider: ${error instanceof Error ? error.message : 'Unknown error'}`,\n        Code.InvalidArgument,\n      )\n    }\n  }\n\n  static validateAsrPrompt(headerValue: string): string {\n    try {\n      return AsrPromptSchema.parse(headerValue)\n    } catch (error) {\n      throw new ConnectError(\n        `Invalid ASR prompt: ${error instanceof Error ? error.message : 'Unknown error'}`,\n        Code.InvalidArgument,\n      )\n    }\n  }\n\n  static validateLlmProvider(headerValue: string): string {\n    try {\n      return LlmProviderSchema.parse(headerValue)\n    } catch (error) {\n      throw new ConnectError(\n        `Invalid LLM provider: ${error instanceof Error ? error.message : 'Unknown error'}`,\n        Code.InvalidArgument,\n      )\n    }\n  }\n\n  static validateLlmModel(headerValue: string): string {\n    try {\n      return LlmModelSchema.parse(headerValue)\n    } catch (error) {\n      throw new ConnectError(\n        `Invalid LLM model: ${error instanceof Error ? error.message : 'Unknown error'}`,\n        Code.InvalidArgument,\n      )\n    }\n  }\n\n  static validateLlmTemperature(headerValue: number): number {\n    try {\n      return LLMTemperatureSchema.parse(headerValue)\n    } catch (error) {\n      throw new ConnectError(\n        `Invalid LLM temperature: ${error instanceof Error ? error.message : 'Unknown error'}`,\n        Code.InvalidArgument,\n      )\n    }\n  }\n\n  static validateTranscriptionPrompt(headerValue: string): string {\n    try {\n      console.log(\n        'Validating transcription prompt:',\n        headerValue.slice(0, 50) + '...',\n      )\n      return LlmPromptSchema.parse(headerValue)\n    } catch (error) {\n      throw new ConnectError(\n        `Invalid transcription prompt: ${error instanceof Error ? error.message : 'Unknown error'}`,\n        Code.InvalidArgument,\n      )\n    }\n  }\n\n  static validateEditingPrompt(headerValue: string): string {\n    try {\n      return LlmPromptSchema.parse(headerValue)\n    } catch (error) {\n      throw new ConnectError(\n        `Invalid editing prompt: ${error instanceof Error ? error.message : 'Unknown error'}`,\n        Code.InvalidArgument,\n      )\n    }\n  }\n\n  static validateNoSpeechThreshold(headerValue: number): number {\n    try {\n      return NoSpeechThresholdSchema.parse(headerValue)\n    } catch (error) {\n      throw new ConnectError(\n        `Invalid no speech threshold: ${error instanceof Error ? error.message : 'Unknown error'}`,\n        Code.InvalidArgument,\n      )\n    }\n  }\n\n  static validateVocabulary(headerValue: string): string[] {\n    try {\n      return VocabularySchema.parse(headerValue)\n    } catch (error) {\n      throw new ConnectError(\n        `Invalid vocabulary: ${error instanceof Error ? error.message : 'Unknown error'}`,\n        Code.InvalidArgument,\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/validation/schemas.test.ts",
    "content": "import { describe, it, expect } from 'bun:test'\nimport {\n  AsrModelSchema,\n  VocabularySchema,\n  VocabularyWordSchema,\n} from './schemas.js'\n\ndescribe('AsrModelSchema', () => {\n  it('should accept valid ASR model names', () => {\n    expect(AsrModelSchema.parse('whisper-large-v3')).toBe('whisper-large-v3')\n    expect(AsrModelSchema.parse('distil-whisper-large-v3-en')).toBe(\n      'distil-whisper-large-v3-en',\n    )\n    expect(AsrModelSchema.parse('custom-model-v1.2')).toBe('custom-model-v1.2')\n  })\n\n  it('should trim whitespace', () => {\n    expect(AsrModelSchema.parse('  whisper-large-v3  ')).toBe(\n      'whisper-large-v3',\n    )\n  })\n\n  it('should throw for undefined input', () => {\n    expect(() => AsrModelSchema.parse(undefined)).toThrow()\n  })\n\n  it('should reject empty strings', () => {\n    expect(() => AsrModelSchema.parse('')).toThrow('ASR model cannot be empty')\n  })\n\n  it('should reject models that are too long', () => {\n    const longModel = 'a'.repeat(101)\n    expect(() => AsrModelSchema.parse(longModel)).toThrow('ASR model too long')\n  })\n\n  it('should reject models with invalid characters', () => {\n    expect(() => AsrModelSchema.parse('model<script>')).toThrow(\n      'ASR model contains invalid characters',\n    )\n    expect(() => AsrModelSchema.parse('model with spaces')).toThrow(\n      'ASR model contains invalid characters',\n    )\n    expect(() => AsrModelSchema.parse('model&dangerous')).toThrow(\n      'ASR model contains invalid characters',\n    )\n  })\n\n  it('should accept models with valid characters', () => {\n    expect(AsrModelSchema.parse('model-name')).toBe('model-name')\n    expect(AsrModelSchema.parse('model_name')).toBe('model_name')\n    expect(AsrModelSchema.parse('model.name')).toBe('model.name')\n    expect(AsrModelSchema.parse('model123')).toBe('model123')\n  })\n})\n\ndescribe('VocabularyWordSchema', () => {\n  it('should accept valid vocabulary words', () => {\n    expect(VocabularyWordSchema.parse('hello')).toBe('hello')\n    expect(VocabularyWordSchema.parse('world-123')).toBe('world-123')\n    expect(VocabularyWordSchema.parse(\"it's\")).toBe(\"it's\")\n    expect(VocabularyWordSchema.parse('multi word')).toBe('multi word')\n  })\n\n  it('should trim whitespace', () => {\n    expect(VocabularyWordSchema.parse('  hello  ')).toBe('hello')\n  })\n\n  it('should reject empty words', () => {\n    expect(() => VocabularyWordSchema.parse('')).toThrow()\n    expect(() => VocabularyWordSchema.parse('   ')).toThrow()\n  })\n\n  it('should reject words that are too long', () => {\n    const longWord = 'a'.repeat(101)\n    expect(() => VocabularyWordSchema.parse(longWord)).toThrow()\n  })\n\n  it('should reject words with invalid characters', () => {\n    expect(() => VocabularyWordSchema.parse('word<script>')).toThrow()\n    expect(() => VocabularyWordSchema.parse('word&dangerous')).toThrow()\n  })\n})\n\ndescribe('VocabularySchema', () => {\n  it('should parse comma-separated vocabulary list', () => {\n    const result = VocabularySchema.parse('hello,world,test')\n    expect(result).toEqual(['hello', 'world', 'test'])\n  })\n\n  it('should handle empty input', () => {\n    expect(VocabularySchema.parse('')).toEqual([])\n  })\n\n  it('should throw for undefined input', () => {\n    expect(() => VocabularySchema.parse(undefined)).toThrow()\n  })\n\n  it('should trim individual words', () => {\n    const result = VocabularySchema.parse('  hello  ,  world  ,  test  ')\n    expect(result).toEqual(['hello', 'world', 'test'])\n  })\n\n  it('should filter out empty words', () => {\n    const result = VocabularySchema.parse('hello,,world,  ,test')\n    expect(result).toEqual(['hello', 'world', 'test'])\n  })\n\n  it('should limit to 500 words', () => {\n    const words = Array.from({ length: 600 }, (_, i) => `word${i}`).join(',')\n    const result = VocabularySchema.parse(words)\n    expect(result).toHaveLength(500)\n  })\n\n  it('should reject if total string is too long', () => {\n    const longString = 'a'.repeat(5001)\n    expect(() => VocabularySchema.parse(longString)).toThrow(\n      'Vocabulary list too long',\n    )\n  })\n\n  it('should filter out invalid words', () => {\n    const result = VocabularySchema.parse(\n      'hello,<script>,world,valid&word,test',\n    )\n    expect(result).toEqual(['hello', 'world', 'test'])\n  })\n\n  it('should handle mixed valid and invalid words', () => {\n    const result = VocabularySchema.parse(\n      'valid1,invalid<>,valid2,too-long-' + 'a'.repeat(100),\n    )\n    expect(result).toEqual(['valid1', 'valid2'])\n  })\n})\n"
  },
  {
    "path": "server/src/validation/schemas.ts",
    "content": "import { z } from 'zod'\nimport { ClientProvider } from '../clients/providers.js'\n\n// ASR model schema - allows known models or any string matching pattern\nexport const AsrModelSchema = z\n  .string()\n  .transform(val => val.trim())\n  .refine(val => val.length > 0, 'ASR model cannot be empty')\n  .refine(val => val.length <= 100, 'ASR model too long')\n  .refine(\n    val => /^[a-zA-Z0-9\\-_.]+$/.test(val),\n    'ASR model contains invalid characters',\n  )\n\nexport const AsrProviderSchema = z.preprocess(\n  val => (typeof val === 'string' ? val.trim() : val),\n  z.enum([ClientProvider.GROQ]),\n)\n\nexport const AsrPromptSchema = z.string().trim().max(100, 'ASR prompt too long')\n\nexport const LlmProviderSchema = z.preprocess(\n  val => (typeof val === 'string' ? val.trim() : val),\n  z.enum([ClientProvider.GROQ, ClientProvider.CEREBRAS]),\n)\n\nexport const LlmModelSchema = z\n  .string()\n  .transform(val => val.trim())\n  .refine(val => val.length > 0, 'LLM model cannot be empty')\n  .refine(val => val.length <= 100, 'LLM model too long')\n  .refine(\n    val => /^[a-zA-Z0-9\\-_./]+$/.test(val),\n    'LLM model contains invalid characters',\n  )\n\nexport const LLMTemperatureSchema = z\n  .number()\n  .min(0, 'Temperature must be at least 0')\n  .max(2, 'Temperature cannot exceed 2')\n\nexport const LlmPromptSchema = z\n  .string()\n  .trim()\n  .max(1500, 'LLM prompt too long')\n\nexport const NoSpeechThresholdSchema = z\n  .number()\n  .min(0, 'No speech probability must be at least 0')\n  .max(1, 'No speech probability cannot exceed 1')\n\n// Individual vocabulary word schema\nexport const VocabularyWordSchema = z\n  .string()\n  .trim()\n  .min(1)\n  .max(100)\n  .regex(/^[a-zA-Z0-9\\-_.\\s']+$/, 'Invalid vocabulary word characters')\n\n// Vocabulary list schema\nexport const VocabularySchema = z\n  .string()\n  .trim()\n  .max(5000, 'Vocabulary list too long')\n  .transform(str => {\n    if (!str) return []\n\n    return str\n      .split(',')\n      .map(word => word.trim())\n      .filter(word => word.length > 0)\n      .slice(0, 500) // Limit number of words\n      .filter(word => {\n        // Validate each word individually\n        try {\n          VocabularyWordSchema.parse(word)\n          return true\n        } catch {\n          return false\n        }\n      })\n  })\n\n// Header validation schema\nexport const HeaderSchema = z.object({\n  asrModel: AsrModelSchema.optional(),\n  vocabulary: VocabularySchema.optional(),\n})\n\nexport type ValidatedHeaders = z.infer<typeof HeaderSchema>\n"
  },
  {
    "path": "server/test-client.ts",
    "content": "import { createClient } from '@connectrpc/connect'\nimport { createConnectTransport } from '@connectrpc/connect-node'\nimport {\n  ItoService,\n  CreateNoteRequestSchema,\n  GetNoteRequestSchema,\n  ListNotesRequestSchema,\n  UpdateNoteRequestSchema,\n  DeleteNoteRequestSchema,\n  CreateInteractionRequestSchema,\n  GetInteractionRequestSchema,\n  ListInteractionsRequestSchema,\n  UpdateInteractionRequestSchema,\n  DeleteInteractionRequestSchema,\n  CreateDictionaryItemRequestSchema,\n  ListDictionaryItemsRequestSchema,\n  UpdateDictionaryItemRequestSchema,\n  DeleteDictionaryItemRequestSchema,\n} from './src/generated/ito_pb.js'\nimport { create } from '@bufbuild/protobuf'\nimport { v4 as uuidv4 } from 'uuid'\n\n// Mock JWT token for testing (replace with actual Auth0 token)\nconst TEST_JWT_TOKEN = process.env.TEST_JWT_TOKEN || 'your-jwt-token-here'\n\nconst transport = createConnectTransport({\n  baseUrl: 'http://localhost:3000',\n  httpVersion: '1.1', // Use HTTP/1.1 to match the server\n})\n\nconst client = createClient(ItoService, transport)\n\n// Create headers with Auth0 JWT token\nconst createAuthHeaders = (token: string) => {\n  return { authorization: `Bearer ${token}` }\n}\n\n// Test the Notes API with authentication\nasync function testNotesApi() {\n  console.log('\\n📝 Testing Notes API...')\n\n  const headers = createAuthHeaders(TEST_JWT_TOKEN)\n  const testUserId = 'test-user-id-123' // A dummy user id for testing\n\n  // 1. Create a note\n  console.log('  - Creating a new note...')\n  const createRequest = create(CreateNoteRequestSchema, {\n    id: uuidv4(),\n    content: 'This is a test note from the client.',\n  })\n  const createdNote = await client.createNote(createRequest, { headers })\n  console.log('  ✓ Note created successfully')\n\n  const noteId = createdNote.id\n\n  // 2. List notes for the user\n  console.log(`  - Listing notes for user ${testUserId}...`)\n  const listRequest = create(ListNotesRequestSchema, {})\n  const listResponse = await client.listNotes(listRequest, { headers })\n  console.log(`  ✓ Found ${listResponse.notes.length} notes.`)\n  if (!listResponse.notes.some(note => note.id === noteId)) {\n    throw new Error('Newly created note not found in list!')\n  }\n  console.log('  ✓ Newly created note is present in the list.')\n\n  // 3. Get the specific note\n  console.log(`  - Getting note by ID ${noteId}...`)\n  const getRequest = create(GetNoteRequestSchema, { id: noteId })\n  const fetchedNote = await client.getNote(getRequest, { headers })\n  console.log('  ✓ Fetched note successfully.')\n  if (fetchedNote.content !== 'This is a test note from the client.') {\n    throw new Error('Fetched note content does not match created content!')\n  }\n\n  // 4. Update the note\n  console.log(`  - Updating note ID ${noteId}...`)\n  const updateRequest = create(UpdateNoteRequestSchema, {\n    id: noteId,\n    content: 'This is the updated note content.',\n  })\n  const updatedNote = await client.updateNote(updateRequest, { headers })\n  console.log('  ✓ Updated note successfully.')\n  if (updatedNote.content !== 'This is the updated note content.') {\n    throw new Error('Note content was not updated correctly!')\n  }\n\n  // 5. Delete the note\n  console.log('  - Deleting note ID ' + noteId + '...')\n  const deleteRequest = create(DeleteNoteRequestSchema, { id: noteId })\n  await client.deleteNote(deleteRequest, { headers })\n  console.log('  ✓ Delete request sent successfully.')\n\n  // 6. Verify the note is now marked as deleted\n  console.log(`  - Verifying note is marked as deleted in the list...`)\n  const finalList = await client.listNotes(listRequest, { headers })\n  const deletedNote = finalList.notes.find(note => note.id === noteId)\n\n  if (!deletedNote) {\n    throw new Error('Soft-deleted note was not found in the final list!')\n  }\n\n  if (!deletedNote.deletedAt) {\n    throw new Error('Note was found but not marked as deleted!')\n  }\n  console.log(\n    `  ✓ Note is correctly marked as deleted at ${deletedNote.deletedAt}.`,\n  )\n\n  console.log('✓ Notes API tests passed!')\n}\n\nasync function testInteractionsApi() {\n  console.log('\\n🤝 Testing Interactions API...')\n\n  const headers = createAuthHeaders(TEST_JWT_TOKEN)\n  const testUserId = 'test-user-id-123'\n\n  // 1. Create an interaction\n  console.log('  - Creating a new interaction...')\n  const asrOutput = {\n    transcript: 'hello world',\n    words: [\n      { text: 'hello', start: 0, end: 1 },\n      { text: 'world', start: 1, end: 2 },\n    ],\n  }\n  const llmOutput = { response: 'Hello to you too!' }\n\n  const createRequest = create(CreateInteractionRequestSchema, {\n    id: uuidv4(),\n    title: 'Test Interaction',\n    asrOutput: JSON.stringify(asrOutput),\n    llmOutput: JSON.stringify(llmOutput),\n  })\n  const createdInteraction = await client.createInteraction(createRequest, {\n    headers,\n  })\n  console.log('  ✓ Interaction created successfully')\n  const interactionId = createdInteraction.id\n  if (JSON.parse(createdInteraction.asrOutput).transcript !== 'hello world') {\n    throw new Error('ASR output mismatch on create')\n  }\n\n  // 2. List interactions\n  console.log(`  - Listing interactions for user ${testUserId}...`)\n  const listRequest = create(ListInteractionsRequestSchema, {})\n  const listResponse = await client.listInteractions(listRequest, { headers })\n  if (!listResponse.interactions.some(i => i.id === interactionId)) {\n    throw new Error('Newly created interaction not found in list!')\n  }\n  console.log('  ✓ Newly created interaction is present in the list.')\n\n  // 3. Get the specific interaction\n  console.log(`  - Getting interaction by ID ${interactionId}...`)\n  const getRequest = create(GetInteractionRequestSchema, { id: interactionId })\n  const fetchedInteraction = await client.getInteraction(getRequest, {\n    headers,\n  })\n  if (fetchedInteraction.title !== 'Test Interaction') {\n    throw new Error('Fetched interaction title does not match!')\n  }\n  console.log('  ✓ Fetched interaction successfully.')\n\n  // 4. Update the interaction\n  console.log(`  - Updating interaction ID ${interactionId}...`)\n  const updateRequest = create(UpdateInteractionRequestSchema, {\n    id: interactionId,\n    title: 'Updated Test Interaction',\n  })\n  const updatedInteraction = await client.updateInteraction(updateRequest, {\n    headers,\n  })\n  if (updatedInteraction.title !== 'Updated Test Interaction') {\n    throw new Error('Interaction title was not updated correctly!')\n  }\n  console.log('  ✓ Updated interaction successfully.')\n\n  // 5. Delete the interaction\n  console.log(`  - Deleting interaction ID ${interactionId}...`)\n  const deleteRequest = create(DeleteInteractionRequestSchema, {\n    id: interactionId,\n  })\n  await client.deleteInteraction(deleteRequest, { headers })\n  console.log('  ✓ Delete request sent successfully.')\n\n  // 6. Verify the interaction is now marked as deleted\n  console.log(`  - Verifying interaction is marked as deleted in the list...`)\n  const finalList = await client.listInteractions(listRequest, { headers })\n  const deletedInteraction = finalList.interactions.find(\n    i => i.id === interactionId,\n  )\n\n  if (!deletedInteraction) {\n    throw new Error('Soft-deleted interaction was not found in the final list!')\n  }\n\n  if (!deletedInteraction.deletedAt) {\n    throw new Error('Interaction was found but not marked as deleted!')\n  }\n  console.log(\n    `  ✓ Interaction is correctly marked as deleted at ${deletedInteraction.deletedAt}.`,\n  )\n\n  console.log('✓ Interactions API tests passed!')\n}\n\nasync function testDictionaryApi() {\n  console.log('\\n📚 Testing Dictionary API...')\n\n  const headers = createAuthHeaders(TEST_JWT_TOKEN)\n  const testUserId = 'test-user-id-123'\n\n  // 1. Create a dictionary item\n  console.log('  - Creating a new dictionary item...')\n  const createRequest = create(CreateDictionaryItemRequestSchema, {\n    id: uuidv4(),\n    word: 'Ito',\n    pronunciation: 'ee-toh',\n  })\n  const createdItem = await client.createDictionaryItem(createRequest, {\n    headers,\n  })\n  console.log('  ✓ Dictionary item created successfully')\n  const itemId = createdItem.id\n  if (createdItem.word !== 'Ito') {\n    throw new Error('Word mismatch on create')\n  }\n\n  // 2. List dictionary items\n  console.log(`  - Listing dictionary items for user ${testUserId}...`)\n  const listRequest = create(ListDictionaryItemsRequestSchema, {})\n  const listResponse = await client.listDictionaryItems(listRequest, {\n    headers,\n  })\n  if (!listResponse.items.some(i => i.id === itemId)) {\n    throw new Error('Newly created dictionary item not found in list!')\n  }\n  console.log('  ✓ Newly created dictionary item is present in the list.')\n\n  // 3. Update the dictionary item\n  console.log(`  - Updating dictionary item ID ${itemId}...`)\n  const updateRequest = create(UpdateDictionaryItemRequestSchema, {\n    id: itemId,\n    word: 'Ito',\n    pronunciation: 'Eye-toh',\n  })\n  const updatedItem = await client.updateDictionaryItem(updateRequest, {\n    headers,\n  })\n  if (updatedItem.pronunciation !== 'Eye-toh') {\n    throw new Error('Dictionary item pronunciation was not updated correctly!')\n  }\n  console.log('  ✓ Updated dictionary item successfully.')\n\n  // 4. Delete the dictionary item\n  console.log(`  - Deleting dictionary item ID ${itemId}...`)\n  const deleteRequest = create(DeleteDictionaryItemRequestSchema, {\n    id: itemId,\n  })\n  await client.deleteDictionaryItem(deleteRequest, { headers })\n  console.log('  ✓ Delete request sent successfully.')\n\n  // 5. Verify the dictionary item is now marked as deleted\n  console.log(\n    `  - Verifying dictionary item is marked as deleted in the list...`,\n  )\n  const finalList = await client.listDictionaryItems(listRequest, { headers })\n  const deletedItem = finalList.items.find(i => i.id === itemId)\n\n  if (!deletedItem) {\n    throw new Error(\n      'Soft-deleted dictionary item was not found in the final list!',\n    )\n  }\n\n  if (!deletedItem.deletedAt) {\n    throw new Error('Dictionary item was found but not marked as deleted!')\n  }\n  console.log(\n    `  ✓ Dictionary item is correctly marked as deleted at ${deletedItem.deletedAt}.`,\n  )\n\n  console.log('✓ Dictionary API tests passed!')\n}\n\n// Test the public health endpoint using fetch\nasync function testHealthEndpoint() {\n  console.log('Testing public HTTP health endpoint...')\n\n  try {\n    const response = await fetch('http://localhost:3000/health')\n    const data = await response.json()\n    console.log('✓ Public health endpoint response:', data)\n    return data\n  } catch (error) {\n    console.error('✗ Health endpoint test error:', error)\n    throw error\n  }\n}\n\n// Main test function\nasync function runTests() {\n  console.log('='.repeat(70))\n  console.log('🧪 Connect RPC + Auth0 Fastify Integration Test')\n  console.log('='.repeat(70))\n\n  try {\n    console.log('TEST_JWT_TOKEN', TEST_JWT_TOKEN)\n\n    // Test HTTP endpoints first\n    console.log('\\n📡 Testing HTTP Endpoints:')\n    await testHealthEndpoint()\n\n    if (TEST_JWT_TOKEN !== 'your-jwt-token-here') {\n      console.log('\\n🚀 Testing Authenticated Connect RPC calls:')\n      await testNotesApi()\n      await testInteractionsApi()\n      await testDictionaryApi()\n\n      console.log('\\n🎉 All authenticated tests passed!')\n    } else {\n      console.log(\n        '\\n⚠️  To test authenticated RPC calls, set TEST_JWT_TOKEN environment variable',\n      )\n      console.log('   Get a token from your Auth0 application and run:')\n      console.log('   export TEST_JWT_TOKEN=\"your-actual-jwt-token\"')\n      console.log('   npm run test-connect')\n    }\n\n    console.log('\\n✅ All tests completed successfully!')\n  } catch (error) {\n    console.error('\\n❌ Test failed:', error)\n    process.exit(1)\n  }\n\n  process.exit(0)\n}\n\nrunTests()\n"
  },
  {
    "path": "server/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"NodeNext\",\n    \"target\": \"ES2020\",\n    \"moduleResolution\": \"NodeNext\",\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"outDir\": \"./dist\",\n    \"allowJs\": true\n  },\n  \"include\": [\"src/**/*\", \"test-client.ts\", \"scripts/**/*\"],\n  \"ts-node\": {\n    \"esm\": true\n  }\n}\n"
  },
  {
    "path": "shared-constants.js",
    "content": "/**\n * Shared constants for default advanced settings across the Ito monorepo.\n * This file is used by both the Electron app and the server to ensure consistency.\n */\n\nconst DEFAULT_ADVANCED_SETTINGS = {\n  // ASR (Automatic Speech Recognition) settings\n  asrProvider: 'groq',\n  asrModel: 'whisper-large-v3',\n  asrPrompt: '',\n\n  // LLM (Large Language Model) settings\n  llmProvider: 'groq',\n  llmModel: 'openai/gpt-oss-120b',\n  llmTemperature: 0.1,\n\n  // Prompt settings\n  transcriptionPrompt: `You are a real-time Transcript Polisher assistant. Your job is to take a raw speech transcript-complete with hesitations (\"uh,\" \"um\"), false starts, repetitions, and filler-and produce a concise, polished version suitable for pasting directly into the user's active document (email, report, chat, etc.).\n\n- Keep the user's meaning and tone intact: don't introduce ideas or change intent.\n- Remove disfluencies: delete \"uh,\" \"um,\" \"you know,\" repeated words, and false starts.\n- Resolve corrections smoothly: when the speaker self-corrects (\"let's do next week... no, next month\"), choose the final phrasing.\n- Preserve natural phrasing: maintain contractions and informal tone if present, unless clarity demands adjustment.\n- Maintain accuracy: do not invent or omit key details like dates, names, or numbers.\n- Produce clean prose: use complete sentences, correct punctuation, and paragraph breaks only where needed for readability.\n- Operate within a single reply: output only the cleaned text-no commentary, meta-notes, or apologies.\n\nExample\nRaw transcript:\n\"Uhhh, so, I was thinking... maybe we could-uh-shoot for Thursday morning? No, actually, let's aim for the first week of May.\"\n\nCleaned output:\n\"Let's schedule the meeting for the first week of May.\"\n\nWhen you receive a transcript, immediately return the polished version following these rules.\n`,\n  editingPrompt: ` You are a Command-Interpreter assistant. Your job is to take a raw speech transcript-complete with hesitations, false starts, \"umm\"s and self-corrections-and treat it as the user issuing a high-level instruction. Instead of merely polishing their words, you must:\n    1.\tExtract the intent: identify the action the user is asking for (e.g. \"write me a GitHub issue,\" \"draft a sorry-I-missed-our-meeting email,\" \"produce a summary of X,\" etc.).\n    2.\tIgnore disfluencies: strip out \"uh,\" \"um,\" false starts and filler so you see only the core command.\n    3.\tMap to a template: choose an appropriate standard format (GitHub issue markdown template, professional email, bullet-point agenda, etc.) that matches the intent.\n    4.\tGenerate the deliverable: produce a fully-formed document in that format, filling in placeholders sensibly from any details in the transcript.\n    5.\tDo not add new intent: if the transcript doesn't specify something (e.g. title, recipients, date), use reasonable defaults (e.g. \"Untitled Issue,\" \"To: [Recipient]\") or prompt the user for the missing piece.\n    6.\tProduce only the final document: no commentary, apologies, or side-notes-just the completed issue/email/summary/etc.\n    7. Your response MUST contain ONLY the resultant text. DO NOT include:\n      - Any markers like [START/END CURRENT NOTES CONTENT]\n      - Any explanations, apologies, or additional text\n      - Any formatting markers like --- or \\`\\`\\`\n  `,\n\n  // Audio quality thresholds\n  noSpeechThreshold: 0.6,\n}\n\nmodule.exports = { DEFAULT_ADVANCED_SETTINGS }\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  darkMode: ['class'],\n  content: ['./app/**/*.{ts,tsx}'],\n  theme: {\n    extend: {\n      fontFamily: {\n        sans: ['Inter', 'system-ui', 'sans-serif'],\n      },\n      keyframes: {\n        'slide-up': {\n          '0%': {\n            transform: 'translateY(100%)',\n            opacity: '0',\n            width: '0',\n            overflow: 'hidden',\n          },\n          '100%': {\n            transform: 'translateY(0)',\n            opacity: '1',\n            width: 'auto',\n            overflow: 'visible',\n          },\n        },\n      },\n      animation: {\n        'slide-up': 'slide-up 0.4s ease-out',\n      },\n    },\n  },\n  plugins: [],\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"files\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.node.json\" },\n    { \"path\": \"./tsconfig.web.json\" }\n  ]\n}\n"
  },
  {
    "path": "tsconfig.node.json",
    "content": "{\n  \"extends\": \"@electron-toolkit/tsconfig/tsconfig.node.json\",\n  \"include\": [\n    \"lib/main/index.d.ts\",\n    \"vite-env.d.ts\",\n    \"electron.vite.config.*\",\n    \"lib/**/*\",\n    \"resources/**/*\",\n    \"app/**/*\"\n  ],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"moduleResolution\": \"bundler\",\n    \"types\": [\"electron-vite/node\", \"bun-types\"],\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  }\n}\n"
  },
  {
    "path": "tsconfig.web.json",
    "content": "{\n  \"extends\": \"@electron-toolkit/tsconfig/tsconfig.web.json\",\n  \"include\": [\n    \"app/index.d.ts\",\n    \"app/**/*\",\n    \"lib/**/*\",\n    \"lib/preload/*.d.ts\",\n    \"resources/**/*\"\n  ],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"jsx\": \"react-jsx\",\n    \"baseUrl\": \".\",\n    \"types\": [\"electron-vite/node\"],\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  }\n}\n"
  },
  {
    "path": "vite-env.d.ts",
    "content": "interface ImportMetaEnv {\n  readonly VITE_AUTH0_DOMAIN: string\n  readonly VITE_AUTH0_CLIENT_ID: string\n  readonly VITE_AUTH0_AUDIENCE: string\n  readonly VITE_GRPC_BASE_URL: string\n  readonly VITE_POSTHOG_API_KEY: string\n  readonly VITE_POSTHOG_HOST: string\n  readonly VITE_UPDATER_BUCKET: string\n  readonly VITE_LOCAL_SERVER_PORT?: string\n  readonly VITE_ITO_VERSION: string\n}\n\ninterface ImportMeta {\n  readonly env: ImportMetaEnv\n}\n\ndeclare module '*.webm' {\n  const src: string\n  export default src\n}\n"
  }
]